Как использовать неинтегрированные 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, поэтому мои вопросы:
Имеет ли смысл использовать этот подход вместо простого синхронного вызова не поточно-ориентированных API-интерфейсов в ASP.NET (главная цель состоит в том, чтобы не жертвовать масштабируемостью)?
Есть ли альтернативные подходы, кроме использования синхронных вызовов API или вообще избегания этих APis?
Кто-нибудь использовал что-то подобное в проектах ASP.NET MVC или Web API и готов поделиться своим опытом?
Буду признателен за любые советы о том, как провести стресс-тестирование и профилировать этот подход с 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, когда это необходимо.
Вы не должны переплетать многопоточность с асинхронностью. Проблема с объектом, который не является потокобезопасным, заключается в том, что один и тот же экземпляр (или статический) доступен нескольким потокам одновременно. При асинхронных вызовах к контексту, возможно, обращаются из другого потока в продолжении, но никогда в одно и то же время (если он не используется несколькими запросами, но это не очень хорошо с самого начала).