Como usar as APIs e padrões não-thread-safe do async / await com a API da Web do ASP.NET?

Essa questão foi acionada pelo contexto de dados EF – Async / Await e Multithreading . Eu respondi a essa pergunta, mas não forneci nenhuma solução definitiva.

O problema original é que há muitas APIs úteis do .NET lá fora (como o DbContext do Microsoft Entity Framework), que fornecem methods asynchronouss projetados para serem usados ​​com o await , mas são documentados como não seguros para thread . Isso os torna ótimos para uso em aplicativos de interface do usuário de desktop, mas não para aplicativos do lado do servidor. [EDITADO] Isso pode não se aplicar ao DbContext , aqui está a declaração da Microsoft sobre a segurança do thread EF6 , julgue por si mesmo. [/EDITADO]

Existem também alguns padrões de código estabelecidos que caem na mesma categoria, como chamar um proxy de serviço WCF com OperationContextScope ( aqui e aqui ), por exemplo:

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

Isso pode falhar porque OperationContextScope usa armazenamento local de thread em sua implementação.

A origem do problema é o AspNetSynchronizationContext que é usado em páginas ASP.NET assíncronas para atender a mais solicitações HTTP com menos segmentos do conjunto de encadeamentos do ASP.NET . Com AspNetSynchronizationContext , uma continuação de await pode ser enfileirada em um encadeamento diferente daquele que iniciou a operação assíncrona, enquanto o encadeamento original é liberado para o pool e pode ser usado para servir outra solicitação HTTP. Isso melhora substancialmente a escalabilidade de código do lado do servidor. O mecanismo é descrito em grandes detalhes em É tudo sobre o SynchronizationContext , uma leitura obrigatória. Portanto, embora não haja access simultâneo à API envolvido, uma possível troca de thread ainda nos impede de usar as APIs mencionadas anteriormente.

Eu estive pensando em como resolver isso sem sacrificar a escalabilidade. Aparentemente, a única maneira de ter essas APIs de volta é manter a afinidade de encadeamento para o escopo das chamadas assíncronas potencialmente afetadas por um comutador de encadeamento.

Digamos que tenhamos essa afinidade de thread. A maioria dessas chamadas são vinculadas a E / S de qualquer maneira ( não há thread ). Enquanto uma tarefa assíncrona está pendente, o encadeamento em que ela foi originada pode ser usado para servir uma continuação de outra tarefa semelhante, cujo resultado já está disponível. Assim, não deve prejudicar muito a escalabilidade. Essa abordagem não é novidade, na verdade, um modelo single-threaded semelhante é usado com sucesso pelo Node.js. IMO, essa é uma daquelas coisas que tornam o Node.js tão popular.

Não vejo porque essa abordagem não pode ser usada no contexto do ASP.NET. Um agendador de tarefas personalizado (vamos chamá-lo de ThreadAffinityTaskScheduler ) pode manter um conjunto separado de encadeamentos de “apartamento de afinidade” para melhorar ainda mais a escalabilidade. Uma vez que a tarefa tenha sido enfileirada para um desses threads de “apartamento”, todos await continuações dentro da tarefa ocorram no mesmo thread.

Veja como uma API que não seja thread-safe da questão vinculada pode ser usada com tal 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); 

Eu fui em frente e implementei o ThreadAffinityTaskScheduler como uma prova de conceito , baseado no excelente StaTaskScheduler do Stephen Toub. Os threads de pool mantidos por ThreadAffinityTaskScheduler não são thread STA no sentido COM clássico, mas implementam a afinidade de thread para continuações de await ( SingleThreadSynchronizationContext é responsável por isso).

Até agora, testei esse código como aplicativo de console e parece funcionar como planejado. Ainda não testei dentro de uma página ASP.NET. Eu não tenho muita experiência em desenvolvimento de ASP.NET em produção, então minhas perguntas são:

  1. Faz sentido usar essa abordagem sobre a simples chamada síncrona de APIs que não são thread-safe no ASP.NET (o objective principal é evitar sacrificar a escalabilidade)?

  2. Existem abordagens alternativas, além de usar invocações de API síncrona ou evitar esses APIs?

  3. Alguém já usou algo semelhante em projetos ASP.NET MVC ou API da Web e está pronto para compartilhar sua experiência?

  4. Qualquer conselho sobre como testar e definir o perfil dessa abordagem com o ASP.NET seria apreciado.

    O Entity Framework irá (deve) manipular saltos de threads através de pontos de await bem; se não, então isso é um bug no EF. OTOH, OperationContextScope é baseado em TLS e não está à await segurança.

    1. APIs síncronas mantêm seu contexto ASP.NET; Isso inclui coisas como identidade e cultura do usuário que são importantes durante o processamento. Além disso, várias APIs do ASP.NET assumem que estão sendo executadas em um contexto ASP.NET real (não estou falando apenas do uso de HttpContext.Current ; na verdade, AspNetSynchronizationContext que SynchronizationContext.Current seja uma instância do AspNetSynchronizationContext ).

    2-3. Eu usei meu próprio contexto single-threaded nested diretamente no contexto do ASP.NET, na tentativa de obter ações filho do MVC async trabalhando sem ter que duplicar o código. No entanto, além de perder os benefícios de escalabilidade (para essa parte da solicitação, pelo menos), você também corre para as APIs do ASP.NET, assumindo que elas estão sendo executadas em um contexto do ASP.NET.

    Então, eu nunca usei essa abordagem na produção. Acabei usando as APIs síncronas quando necessário.

    Você não deve entrelaçar o multithreading com assincronia. O problema de um object não ser thread-safe é quando uma única instância (ou estática) é acessada por vários threads ao mesmo tempo . Com as chamadas assíncronas, o contexto é possivelmente acessado a partir de um thread diferente na continuação, mas nunca ao mesmo tempo (quando não é compartilhado entre várias solicitações, mas isso não é bom em primeiro lugar).