Этот вопрос был вызван Контекст данных EF - Async/Await и многопоточность. Я ответил на этот вопрос, но не дал окончательного решения.
Исходная проблема заключается в том, что там много полезных .NET API (например, Microsoft Entity Framework DbContext
), которые обеспечивают асинхронные методы, предназначенные для использования с await
, но они документируются как не потокобезопасные. Это делает их отличными для использования в приложениях для настольных ПК, но не для приложений на стороне сервера. [EDITED] На самом деле это не относится к DbContext
, вот инструкция Microsoft о безопасности потоков EF6, судья для себя. [/EDITED]
Есть также некоторые установленные шаблоны кода, попадающие в ту же категорию, что и вызов прокси-сервера службы 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
может быть поставлено в очередь в другом потоке из того, который инициировал операцию async, в то время как исходный поток был выпущен в пул и может использоваться для обслуживания другого HTTP-запроса. Это существенно улучшает масштабируемость кода на стороне сервера. Механизм подробно описан в It All About the SynchronizationContext, обязательное чтение. Таким образом, в то время как не существует параллельного доступа к API, потенциальный переключатель потоков по-прежнему не позволяет нам использовать вышеупомянутые API.
Я думал о том, как решить эту проблему, не жертвуя масштабируемостью. По-видимому, единственный способ вернуть эти API-интерфейсы - поддерживать слияние потоков для области асинхронных вызовов, потенциально затронутых коммутатором потока.
Скажем, мы имеем такое сходство потоков. Большинство из этих вызовов в любом случае связаны с IO (Нет нити). Пока задача async находится на рассмотрении, поток, из которого он был создан, может использоваться для продолжения другой аналогичной задачи, результат которой уже доступен. Таким образом, это не должно сильно повредить масштабируемости. Этот подход не является чем-то новым, по сути, аналогичная однопоточная модель успешно используется Node.js. IMO, это одна из тех вещей, которые делают Node.js настолько популярным.
Я не понимаю, почему этот подход нельзя использовать в контексте ASP.NET. Пользовательский планировщик задач (пусть его называют ThreadAffinityTaskScheduler
) может поддерживать отдельный пул потоков "affinity apartment", чтобы еще больше повысить масштабируемость. После того как задача была поставлена в очередь на один из этих "квартирных" потоков, все 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, поэтому мои вопросы:
-
Имеет ли смысл использовать этот подход для простого синхронного вызова небезобезопасных API-интерфейсов в ASP.NET(основная цель - избежать потери масштабируемости)?
-
Есть ли альтернативные подходы, помимо использования синхронных API-запросов или вообще избегающих этих AP?
-
Кто-нибудь использовал что-то подобное в проектах ASP.NET MVC или Web API и готов поделиться своим опытом?
-
Любые советы о том, как стресс-тест и профилировать этот подход с ASP.NET, будут оценены.