Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Quando se trabalha com async e await, dois tipos de contexto desempenham papéis importantes mas muito diferentes: ExecutionContext e SynchronizationContext. Aprendes o que cada um faz, como cada um interage com async/await, e por que SynchronizationContext.Current não flui através dos pontos de espera.
O que é o ExecutionContext?
ExecutionContext é um container para o estado contextual que acompanha o fluxo de controlo lógico do seu programa. Num mundo síncrono, a informação ambiente vive em armazenamento local de threads (TLS), e todo o código a correr num determinado thread vê esses dados. Num mundo assíncrono, uma operação lógica pode começar numa thread, suspender e retomar numa thread diferente. Os dados locais de thread não seguem automaticamente —ExecutionContext fazem com que sigam.
O fluxo do ExecutionContext
Captura ExecutionContext usando ExecutionContext.Capture(). Restaure-o durante a execução de um delegado ao usar ExecutionContext.Run:
static void ExecutionContextCaptureDemo()
{
// Capture the current ExecutionContext
ExecutionContext? ec = ExecutionContext.Capture();
// Later, run a delegate within that captured context
if (ec is not null)
{
ExecutionContext.Run(ec, _ =>
{
// Code here sees the ambient state from the point of capture
Console.WriteLine("Running inside captured ExecutionContext.");
}, null);
}
}
Sub ExecutionContextCaptureExample()
' Capture the current ExecutionContext
Dim ec As ExecutionContext = ExecutionContext.Capture()
' Later, run a delegate within that captured context
If ec IsNot Nothing Then
ExecutionContext.Run(ec,
Sub(state)
' Code here sees the ambient state from the point of capture
Console.WriteLine("Running inside captured ExecutionContext.")
End Sub, Nothing)
End If
End Sub
Todas as APIs assíncronas em .NET que funcionam em fork — Run, QueueUserWorkItem, BeginRead, entre outras, — capturam ExecutionContext e usam o contexto armazenado ao invocar o seu callback. Este processo de capturar o estado num thread e restaurá-lo noutro é o que significa "flowing ExecutionContext".
O que é o SynchronizationContext?
SynchronizationContext é uma abstração que representa um ambiente-alvo onde queres que o trabalho corra. Diferentes frameworks de interface fornecem as suas próprias implementações:
- Windows Forms fornece
WindowsFormsSynchronizationContext, que substitui Post para chamarControl.BeginInvoke. - WPF fornece
DispatcherSynchronizationContext, que substitui Post para chamarDispatcher.BeginInvoke. - ASP.NET (no .NET Framework) forneceu o seu próprio contexto que garantiu que
HttpContext.Currentestava disponível.
Ao usar SynchronizationContext , em vez de APIs de marshaling específicas do framework, pode escrever componentes que funcionam entre frameworks de interface:
static class SyncContextExample
{
public static void DoWork()
{
// Capture the current SynchronizationContext
SynchronizationContext? sc = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_ =>
{
// ... do work on the ThreadPool ...
if (sc is not null)
{
sc.Post(_ =>
{
// This runs on the original context (e.g. UI thread)
Console.WriteLine("Back on the original context.");
}, null);
}
});
}
}
Class SyncContextExample
Public Shared Sub DoWork()
' Install a custom SynchronizationContext for demonstration
Dim customContext As New SimpleSynchronizationContext()
SynchronizationContext.SetSynchronizationContext(customContext)
' Capture the current SynchronizationContext
Dim sc As SynchronizationContext = SynchronizationContext.Current
ThreadPool.QueueUserWorkItem(
Sub(state)
' ... do work on the ThreadPool ...
If sc IsNot Nothing Then
sc.Post(
Sub(s)
' This runs on the original context (e.g. UI thread)
Console.WriteLine("Back on the original context.")
End Sub, Nothing)
Else
Console.WriteLine("No SynchronizationContext was captured.")
End If
End Sub)
End Sub
End Class
' A minimal SynchronizationContext for demonstration purposes
Class SimpleSynchronizationContext
Inherits SynchronizationContext
Public Overrides Sub Post(d As SendOrPostCallback, state As Object)
' Queue the callback to run on a thread pool thread
ThreadPool.QueueUserWorkItem(
Sub(s)
d(state)
End Sub)
End Sub
End Class
Capturar um Contexto de Sincronização
Quando capturas um SynchronizationContext, lês a referência de SynchronizationContext.Current e guardas-a para uso posterior. Depois, utilizas Post com a referência capturada para agendar novamente o trabalho nesse ambiente.
Execução de ExecutionContext vs. utilização de SynchronizationContext
Embora ambos os mecanismos envolvam capturar estado a partir de um thread, servem propósitos diferentes:
- Flowing ExecutionContext significa capturar o estado ambiente e tornar esse mesmo estado atual durante a execução de um delegado. O delegado candidata-se onde quer que acabe por concorrer — o estado segue-o.
- Usar o SynchronizationContext significa capturar um destino de agendamento e usá-lo para decidir onde um delegado executa. O contexto capturado controla onde o delegado corre.
Resumindo: ExecutionContext responde "que ambiente deve ser visível?" enquanto SynchronizationContext responde "onde deve correr o código?"
Como o async/await interage com ambos os contextos
A async/await infraestrutura interage automaticamente com ambos os contextos, mas de formas diferentes.
ExecutionContext está sempre ativo e fluindo
Sempre que um await suspende um método (porque o IsCompleted esperante retorna false), a infraestrutura captura um ExecutionContext. Quando o método recomeça, a continuação decorre dentro do contexto capturado. Este comportamento está incorporado nos construtores de métodos assíncronos (por exemplo, AsyncTaskMethodBuilder) e aplica-se independentemente do tipo de awaitable que uses.
SuppressFlow() existe, mas não é um interruptor específico de espera como ConfigureAwait(false). Suprime ExecutionContext a captura para trabalhos que colocas na fila enquanto a supressão está ativa. Não fornece uma opção por modelo de programação await para que os construtores de métodos assíncronos não restaurem o contexto capturado ExecutionContext durante uma continuação. Esse design é intencional porque ExecutionContext é suporte ao nível da infraestrutura que simula semântica local de threads num mundo assíncrono, e a maioria dos programadores nunca precisa de pensar nisso.
Os esperadores de tarefas capturam SynchronizationContext
Os "awaiters" para Task e Task<TResult> incluem suporte para SynchronizationContext. Os construtores de métodos assíncronos não incluem este suporte.
Quando faz await uma tarefa:
- O empregado verifica SynchronizationContext.Current.
- Se existe um contexto, o esperante capta-o.
- Quando a tarefa termina, a continuação é enviada de volta para esse contexto capturado em vez de ser executada no tópico de conclusão ou no conjunto de tópicos.
Este comportamento é como await "te traz de volta ao que estavas". Por exemplo, retomar o tópico da interface numa aplicação de ambiente de trabalho.
ConfigureAwait controla a captura do contexto de sincronização
Se não quiser o comportamento de organização, ligue ConfigureAwait com false:
await task.ConfigureAwait(false);
Quando defines continueOnCapturedContext para false, o awaiter não verifica o SynchronizationContext e a continuação é executada onde a tarefa for concluída (normalmente num pool de threads). Os autores da biblioteca devem usar ConfigureAwait(false) em cada await, a menos que o código precise especificamente de retomar o contexto capturado.
SynchronizationContext.Current não se propaga com await
Este ponto é o mais importante: SynchronizationContext.Currentnão flui através dos pontos de espera. Os construtores de métodos assíncronos em tempo de execução utilizam sobrecargas internas que suprimem explicitamente SynchronizationContext de fluir como parte de ExecutionContext.
Por que motivo isto é importante
Tecnicamente, SynchronizationContext é um dos subcontextos que ExecutionContext pode conter. Se fluía como parte de ExecutionContext, o código a correr num thread pool pode ver uma interface SynchronizationContext como Current, não porque esse thread seja o thread UI, mas porque o contexto "vazou" via flow. Essa mudança alteraria o significado de SynchronizationContext.Current de "o ambiente em que estou atualmente" para "o ambiente existente historicamente em algum lugar na cadeia de chamadas."
O exemplo Task.Run
Considere código que transfere trabalho para o pool de threads. O comportamento UI-thread descrito aqui aplica-se apenas quando SynchronizationContext.Current não é nulo, como numa aplicação UI:
static class TaskRunExample
{
public static async Task ProcessOnUIThread()
{
// This method is called from a thread with a SynchronizationContext.
// Task.Run offloads work to the thread pool.
string result = await Task.Run(async () =>
{
string data = await DownloadAsync();
// Compute runs on the thread pool, not the original context,
// because SynchronizationContext doesn't flow into Task.Run.
return Compute(data);
});
// Back on the original context (the continuation is posted back).
Console.WriteLine(result);
}
private static async Task<string> DownloadAsync()
{
await Task.Delay(100);
return "downloaded data";
}
private static string Compute(string data) =>
$"Computed: {data.Length} chars";
}
Class TaskRunExampleClass
Public Shared Async Function ProcessOnUIThread() As Task
' If a SynchronizationContext is present when this method starts,
' the outer await captures it. Task.Run still offloads work to the thread pool.
Dim result As String = Await Task.Run(
Async Function()
Dim data As String = Await DownloadAsync()
' Compute runs on the thread pool, not the caller's context,
' because SynchronizationContext doesn't flow into Task.Run.
Return Compute(data)
End Function)
' Resume on the captured context, if one was available.
Console.WriteLine(result)
End Function
Private Shared Async Function DownloadAsync() As Task(Of String)
Await Task.Delay(100)
Return "downloaded data"
End Function
Private Shared Function Compute(data As String) As String
Return $"Computed: {data.Length} chars"
End Function
End Class
Numa aplicação de consola, SynchronizationContext.Current é tipicamente null, por isso o excerto não retoma numa linha de execução real de interface. Em vez disso, o excerto ilustra a regra conceptualmente: se uma interface SynchronizationContext fluísse através await de pontos, o await interior do delegado passado para Task.Run veria esse contexto da interface como Current. A continuação após await DownloadAsync() seria então reenviada para o thread da interface, fazendo com que Compute(data) fosse executado no thread da interface em vez de no pool de threads. Esse comportamento anula o propósito da chamada Task.Run.
Como o runtime suprime SynchronizationContext o fluxo em ExecutionContext, o await interior Task.Run não herda um contexto externo da interface, e a continuação continua a correr no pool de threads como pretendido.
Resumo
| Aspeto | Contexto de Execução | SincronizaçãoContexto |
|---|---|---|
| Objetivo | Transporta o estado ambiental através de fronteiras assíncronas | Representa um agendador de alvo (onde o código deve ser executado) |
| Capturado por | Construtores de métodos assíncronos (infraestrutura) | Esperadores de tarefas (await task) |
| Os fluxos atravessam à espera? | Sim, sempre | Não—capturado e publicado, não transmitido em tempo real |
| API de remoção |
ExecutionContext.SuppressFlow (avançado; raramente necessário) |
ConfigureAwait(false) |
| Scope | Todos os aguardáveis |
Task e Task<TResult> (awaiters personalizados podem adicionar lógica semelhante) |