Как использовать неинтегрированные API-интерфейсы и шаблоны асинхронного / ожидающего доступа с ASP.NET Web API?

Этот вопрос был вызван EF Data Context - Async / Await & Multithreading. Я ответил на этот вопрос, но не дал никакого окончательного решения.

Первоначальная проблема заключается в том, что существует множество полезных.NET API (таких как Microsoft Entity Framework DbContext), которые предоставляют асинхронные методы, предназначенные для использования с await Тем не менее они задокументированы как не поточно-ориентированные. Это делает их отличными для использования в настольных приложениях с пользовательским интерфейсом, но не для серверных приложений. [Отредактировано] Это может на самом деле не относится к DbContext Вот заявление Microsoft о безопасности потоков EF6, судите сами. [/ Под ред]

Есть также некоторые установленные шаблоны кода, попадающие в ту же категорию, например, вызов прокси-сервера службы WCF с OperationContextScope (спрашивается здесь и здесь), например:

using (var docClient = CreateDocumentServiceClient())
using (new OperationContextScope(docClient.InnerChannel))
{
    return await docClient.GetDocumentAsync(docId);
}

Это может потерпеть неудачу, потому что OperationContextScope использует локальную память потока в своей реализации.

Источник проблемы AspNetSynchronizationContext который используется на асинхронных страницах ASP.NET для выполнения большего количества HTTP-запросов с меньшим количеством потоков из ASP.NET пул потоков. С AspNetSynchronizationContext, await продолжение может быть поставлено в очередь в другом потоке, отличном от того, который инициировал асинхронную операцию, в то время как исходный поток освобождается в пул и может использоваться для обслуживания другого HTTP-запроса. Это существенно улучшает масштабируемость кода на стороне сервера. Механизм подробно описан в "Все о SynchronizationContext", который необходимо прочитать. Таким образом, хотя одновременный доступ к API не задействован, потенциальное переключение потоков все еще не позволяет нам использовать вышеупомянутые API.

Я думал о том, как решить эту проблему, не жертвуя масштабируемостью. По-видимому, единственный способ вернуть эти API-интерфейсы состоит в том, чтобы поддерживать сходство потоков с областью асинхронных вызовов, потенциально подверженных переключению потоков.

Допустим, у нас есть такая нить сродства. В любом случае, большинство из этих вызовов связаны с IO ( нет потока). Пока асинхронная задача находится в состоянии ожидания, поток, в котором она была создана, может использоваться для продолжения другой аналогичной задачи, результат которой уже доступен. Таким образом, это не должно сильно повредить масштабируемости. В этом подходе нет ничего нового, на самом деле, подобная однопоточная модель успешно используется Node.js. ИМО, это одна из тех вещей, которые делают Node.js таким популярным.

Я не понимаю, почему этот подход не может быть использован в контексте ASP.NET. Пользовательский планировщик задач (назовем его ThreadAffinityTaskScheduler) может поддерживать отдельный пул потоков "квартира соответствия", чтобы еще больше улучшить масштабируемость. Как только задача была поставлена ​​в очередь в один из этих "квартирных" потоков, все await продолжения внутри задачи будут происходить в том же потоке.

Вот как можно использовать не-поточно-безопасный API из связанного вопроса с такими ThreadAffinityTaskScheduler:

// create a global instance of ThreadAffinityTaskScheduler - per web app
public static class GlobalState 
{
    public static ThreadAffinityTaskScheduler TaScheduler { get; private set; }

    public static GlobalState 
    {
        GlobalState.TaScheduler = new ThreadAffinityTaskScheduler(
            numberOfThreads: 10);
    }
}

// ...

// run a task which uses non-thread-safe APIs
var result = await GlobalState.TaScheduler.Run(() => 
{
    using (var dataContext = new DataContext())
    {
        var something = await dataContext.someEntities.FirstOrDefaultAsync(e => e.Id == 1);
        var morething = await dataContext.someEntities.FirstOrDefaultAsync(e => e.Id == 2);
        // ... 
        // transform "something" and "morething" into thread-safe objects and return the result
        return data;
    }
}, CancellationToken.None);

Я пошел вперед и реализовал ThreadAffinityTaskScheduler в качестве доказательства концепции, основанной на превосходном Стивена Тауба StaTaskScheduler, Пулы потоков поддерживаются ThreadAffinityTaskScheduler не являются потоком STA в классическом смысле COM, но они реализуют сходство потоков для await продолжения (SingleThreadSynchronizationContext несет ответственность за это).

До сих пор я тестировал этот код как консольное приложение, и он, кажется, работает как задумано. Я еще не тестировал его на странице ASP.NET. У меня нет большого опыта разработки ASP.NET, поэтому мои вопросы:

  1. Имеет ли смысл использовать этот подход вместо простого синхронного вызова не поточно-ориентированных API-интерфейсов в ASP.NET (главная цель состоит в том, чтобы не жертвовать масштабируемостью)?

  2. Есть ли альтернативные подходы, кроме использования синхронных вызовов API или вообще избегания этих APis?

  3. Кто-нибудь использовал что-то подобное в проектах ASP.NET MVC или Web API и готов поделиться своим опытом?

  4. Буду признателен за любые советы о том, как провести стресс-тестирование и профилировать этот подход с ASP.NET.

2 ответа

Решение

Entity Framework будет (должен) обрабатывать потоки через await очки просто отлично; если это не так, то это ошибка в EF. Ото, OperationContextScope основан на TLS и не await-безопасный.

1. Синхронные API поддерживают ваш контекст ASP.NET; это включает в себя такие вещи, как личность пользователя и культура, которые часто важны во время обработки. Кроме того, некоторые API ASP.NET предполагают, что они работают в реальном контексте ASP.NET (я не имею в виду просто использование HttpContext.Current; Я имею в виду, фактически предполагая, что SynchronizationContext.Current это пример AspNetSynchronizationContext).

2-3. Я использовал свой собственный однопоточный контекст, вложенный непосредственно в контекст ASP.NET, в попытках получить async Дочерние действия MVC работают без дублирования кода. Однако вы не только теряете преимущества масштабируемости (по крайней мере, для этой части запроса), вы также сталкиваетесь с API-интерфейсами ASP.NET, предполагая, что они работают в контексте ASP.NET.

Итак, я никогда не использовал этот подход в производстве. Я просто использую синхронные API, когда это необходимо.

Вы не должны переплетать многопоточность с асинхронностью. Проблема с объектом, который не является потокобезопасным, заключается в том, что один и тот же экземпляр (или статический) доступен нескольким потокам одновременно. При асинхронных вызовах к контексту, возможно, обращаются из другого потока в продолжении, но никогда в одно и то же время (если он не используется несколькими запросами, но это не очень хорошо с самого начала).

Другие вопросы по тегам