Подтвердить что ты не робот

Как использовать API-интерфейсы и шаблоны async/await без потоков для ASP.NET Web API?

Этот вопрос был вызван Контекст данных 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, будут оценены.

4b9b3361

Ответ 1

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

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, если это необходимо.

Ответ 2

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