Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Primitivos de coordenação síncrona, como ManualResetEventSlim, CountdownEvente Barrier bloqueiam o thread de chamada enquanto aguardam. No código assíncrono, bloquear um thread desperdiça um recurso que pode estar fazendo outro trabalho. Use TaskCompletionSource para criar equivalentes assíncronos que permitam que os chamadores await em vez de bloquear.
Um TaskCompletionSource produz um Task que você conclui manualmente chamando SetResult(), SetExceptionou SetCanceled. O código que aguarda essa tarefa suspende sem bloquear uma thread e retoma quando você conclui a origem. Esse padrão forma o bloco de construção para cada primitivo neste artigo.
Note
As primitivas neste artigo são implementações educacionais. Para limitação de taxa e exclusão mútua em produção, use os tipos integrados abordados em Semáforos assíncronos, bloqueios e coordenação de leitor/gravador. Conclua sempre cada TaskCompletionSource que você criar; veja Concluindo suas tarefas para mais informações.
Evento de reinicialização manual assíncrona
Um evento de reinicialização manual inicia em um estado não sinalizado. Os chamadores aguardam o evento e todos os que aguardam retomam quando outra parte sinaliza (define) o evento. O evento permanece sinalizado até que você o reinicie explicitamente. O equivalente síncrono é ManualResetEventSlim. O runtime do .NET fornece TaskCompletionSource<TResult> diretamente para sinalização de transmissão única. Crie uma nova instância a cada ciclo em vez de criar um wrapper de reinicialização em torno dela.
TaskCompletionSource é um evento de reinicialização manual de execução única: seu Task permanece incompleto até que um método Set* seja chamado, e então todos os aguardadores são retomados. Adicione um método Reset que substitua por um novo TaskCompletionSource, e você terá um evento de redefinição manual assíncrono reutilizável.
// Educational only — use TaskCompletionSource<T> directly instead of this sample implementation; create a new instance each cycle.
public class AsyncManualResetEvent
{
private volatile TaskCompletionSource _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
public Task WaitAsync() => _tcs.Task;
public void Set() => _tcs.TrySetResult();
public void Reset()
{
while (true)
{
TaskCompletionSource tcs = _tcs;
if (!tcs.Task.IsCompleted ||
Interlocked.CompareExchange(
ref _tcs,
new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously),
tcs) == tcs)
{
return;
}
}
}
}
' Educational only — use TaskCompletionSource(Of T) directly instead of this sample implementation; create a new instance each cycle.
Public Class AsyncManualResetEvent
Private _tcs As TaskCompletionSource = New TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)
Public Function WaitAsync() As Task
Return _tcs.Task
End Function
Public Sub [Set]()
_tcs.TrySetResult()
End Sub
Public Sub Reset()
Do
Dim tcs As TaskCompletionSource = _tcs
If Not tcs.Task.IsCompleted OrElse
Interlocked.CompareExchange(
_tcs,
New TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously),
tcs) Is tcs Then
Return
End If
Loop
End Sub
End Class
Principais detalhes de implementação:
- O construtor passa TaskCreationOptions.RunContinuationsAsynchronously para impedir que
Setexecute continuações de espera de forma síncrona na thread de chamada. Sem esse sinalizador,Setpoderia bloquear por um período imprevisível de tempo. -
Resetusa CompareExchange para trocar por um novoTaskCompletionSourcesomente quando o atual já estiver concluído. Essa troca atômica impede que uma tarefa que um aguardador já recebeu seja abandonada.
O exemplo a seguir mostra como duas tarefas são coordenadas por meio do evento:
public static class AsyncManualResetEventDemo
{
public static async Task RunAsync()
{
var gate = new AsyncManualResetEvent();
Task waiter = Task.Run(async () =>
{
Console.WriteLine("Waiter: waiting for signal...");
await gate.WaitAsync();
Console.WriteLine("Waiter: signal received!");
});
await Task.Delay(100);
Console.WriteLine("Signaler: setting the event.");
gate.Set();
await waiter;
}
}
Public Module AsyncManualResetEventDemo
Public Async Function RunAsync() As Task
Dim gate As New AsyncManualResetEvent()
Dim waiter As Task = Task.Run(Async Function()
Console.WriteLine("Waiter: waiting for signal...")
Await gate.WaitAsync()
Console.WriteLine("Waiter: signal received!")
End Function)
Await Task.Delay(100)
Console.WriteLine("Signaler: setting the event.")
gate.Set()
Await waiter
End Function
End Module
Evento de reinicialização automática assíncrona
Um evento de reinicialização automática é semelhante a um evento de reinicialização manual, mas retorna automaticamente ao estado não sinalizado após liberar exatamente um aguardador. Se vários chamadores estiverem aguardando quando o evento for sinalizado, apenas um aguardador será retomado. O equivalente síncrono é AutoResetEvent. O runtime do .NET inclui SemaphoreSlim para sinalização assíncrona de aguardador único. Inicialize-o em 0 com uma contagem máxima de 1 e chame WaitAsync para aguardar e Release para sinalizar.
Como cada sinal libera apenas um garçom, você precisa de uma coleção de TaskCompletionSource instâncias , uma por garçom, para que você possa concluí-las individualmente:
// Educational only — use SemaphoreSlim(0, 1) instead of this sample implementation: call WaitAsync() to wait and Release() to signal.
public class AsyncAutoResetEvent
{
private readonly Queue<TaskCompletionSource> _waiters = new();
private bool _signaled;
public Task WaitAsync()
{
lock (_waiters)
{
if (_signaled)
{
_signaled = false;
return Task.CompletedTask;
}
else
{
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_waiters.Enqueue(tcs);
return tcs.Task;
}
}
}
public void Set()
{
TaskCompletionSource? toRelease = null;
lock (_waiters)
{
if (_waiters.Count > 0)
{
toRelease = _waiters.Dequeue();
}
else if (!_signaled)
{
_signaled = true;
}
}
toRelease?.TrySetResult();
}
}
' Educational only — use SemaphoreSlim(0, 1) instead of this sample implementation: call WaitAsync() to wait and Release() to signal.
Public Class AsyncAutoResetEvent
Private ReadOnly _waiters As New Queue(Of TaskCompletionSource)()
Private _signaled As Boolean
Public Function WaitAsync() As Task
SyncLock _waiters
If _signaled Then
_signaled = False
Return Task.CompletedTask
Else
Dim tcs As New TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)
_waiters.Enqueue(tcs)
Return tcs.Task
End If
End SyncLock
End Function
Public Sub [Set]()
Dim toRelease As TaskCompletionSource = Nothing
SyncLock _waiters
If _waiters.Count > 0 Then
toRelease = _waiters.Dequeue()
ElseIf Not _signaled Then
_signaled = True
End If
End SyncLock
toRelease?.TrySetResult()
End Sub
End Class
Principais detalhes de implementação:
- O método
Setcompleta oTaskCompletionSource(TCS) fora do bloqueio. Completar um TCS dentro do bloqueio executa continuações síncronas enquanto o bloqueio é mantido, o que pode causar impasses ou reentrância inesperada. - Quando
Seté chamado e nenhum esperador está na fila, o sinal é armazenado para que a próxima chamada deWaitAsyncseja concluída imediatamente.
O exemplo a seguir mostra um produtor sinalizando um consumidor por meio do evento:
public static class AsyncAutoResetEventDemo
{
public static async Task RunAsync()
{
var autoEvent = new AsyncAutoResetEvent();
Task consumer = Task.Run(async () =>
{
for (int i = 0; i < 3; i++)
{
await autoEvent.WaitAsync();
Console.WriteLine($"Consumer: received signal {i + 1}");
}
});
for (int i = 0; i < 3; i++)
{
await Task.Delay(50);
Console.WriteLine($"Producer: sending signal {i + 1}");
autoEvent.Set();
}
await consumer;
}
}
Public Module AsyncAutoResetEventDemo
Public Async Function RunAsync() As Task
Dim autoEvent As New AsyncAutoResetEvent()
Dim consumer As Task = Task.Run(Async Function()
For i As Integer = 0 To 2
Await autoEvent.WaitAsync()
Console.WriteLine($"Consumer: received signal {i + 1}")
Next
End Function)
For i As Integer = 0 To 2
Await Task.Delay(50)
Console.WriteLine($"Producer: sending signal {i + 1}")
autoEvent.Set()
Next
Await consumer
End Function
End Module
Evento de contagem regressiva assíncrona
Um evento de contagem regressiva aguarda um número específico de sinais antes de permitir que os esperadores prossigam. Esse padrão é útil para cenários de bifurcação/junção em que você inicia N operações e deseja aguardar todas as N conclusões. O equivalente síncrono é CountdownEvent. O runtime do .NET fornece WhenAll para coordenação de bifurcação/junção com um conjunto fixo de tarefas. Em vez disso, use-o.
Crie a versão assíncrona compondo o AsyncManualResetEvent da seção anterior com um contador atômico.
// Educational only — use Task.WhenAll() instead of this sample implementation to coordinate a fixed set of tasks.
public class AsyncCountdownEvent
{
private readonly AsyncManualResetEvent _event = new();
private int _count;
public AsyncCountdownEvent(int initialCount)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(initialCount, nameof(initialCount));
_count = initialCount;
}
public Task WaitAsync() => _event.WaitAsync();
public void Signal()
{
if (_count <= 0)
throw new InvalidOperationException("The event is already signaled.");
int newCount = Interlocked.Decrement(ref _count);
if (newCount == 0)
_event.Set();
else if (newCount < 0)
throw new InvalidOperationException("Too many signals.");
}
public Task SignalAndWait()
{
Signal();
return WaitAsync();
}
}
' Educational only — use Task.WhenAll() instead of this sample implementation to coordinate a fixed set of tasks.
Public Class AsyncCountdownEvent
Private ReadOnly _event As New AsyncManualResetEvent()
Private _count As Integer
Public Sub New(initialCount As Integer)
If initialCount <= 0 Then Throw New ArgumentOutOfRangeException(NameOf(initialCount))
_count = initialCount
End Sub
Public Function WaitAsync() As Task
Return _event.WaitAsync()
End Function
Public Sub Signal()
If _count <= 0 Then
Throw New InvalidOperationException("The event is already signaled.")
End If
Dim newCount As Integer = Interlocked.Decrement(_count)
If newCount = 0 Then
_event.Set()
ElseIf newCount < 0 Then
Throw New InvalidOperationException("Too many signals.")
End If
End Sub
Public Function SignalAndWait() As Task
Signal()
Return WaitAsync()
End Function
End Class
O Signal método diminui a contagem atomicamente com Decrement. Quando a contagem chega a zero, o evento interno é definido e todos os esperadores retomam suas atividades.
O exemplo a seguir usa um evento de contagem regressiva para aguardar três operações simultâneas:
public static class AsyncCountdownEventDemo
{
public static async Task RunAsync()
{
var countdown = new AsyncCountdownEvent(3);
for (int i = 1; i <= 3; i++)
{
int id = i;
_ = Task.Run(async () =>
{
await Task.Delay(id * 30);
Console.WriteLine($"Worker {id}: done.");
countdown.Signal();
});
}
await countdown.WaitAsync();
Console.WriteLine("All workers finished.");
}
}
Public Module AsyncCountdownEventDemo
Public Async Function RunAsync() As Task
Dim countdown As New AsyncCountdownEvent(3)
For i As Integer = 1 To 3
Dim id As Integer = i
Dim backgroundTask As Task = Task.Run(Async Function()
Await Task.Delay(id * 30)
Console.WriteLine($"Worker {id}: done.")
countdown.Signal()
End Function)
Next
Await countdown.WaitAsync()
Console.WriteLine("All workers finished.")
End Function
End Module
Barreira assíncrona
Uma barreira coordena um conjunto fixo de participantes em várias rodadas. Cada participante sinaliza quando conclui seu trabalho para a rodada atual e aguarda que todos os outros participantes terminem. Quando o último participante sinaliza, todos os participantes retomam suas atividades e a barreira é redefinida para a próxima rodada. O equivalente síncrono é Barrier. O runtime do .NET fornece WhenAll para sincronização assíncrona de múltiplas etapas. Combine-o com um loop, uma chamada de WhenAll por rodada.
// Educational only — use Task.WhenAll() in a loop instead of this sample implementation, one call per round.
public class AsyncBarrier
{
private readonly int _participantCount;
private int _remainingParticipants;
private TaskCompletionSource _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
public AsyncBarrier(int participantCount)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(participantCount, nameof(participantCount));
_remainingParticipants = _participantCount = participantCount;
}
public Task SignalAndWait()
{
TaskCompletionSource tcs = _tcs;
if (Interlocked.Decrement(ref _remainingParticipants) == 0)
{
_remainingParticipants = _participantCount;
_tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
tcs.SetResult();
}
return tcs.Task;
}
}
' Educational only — use Task.WhenAll() in a loop instead of this sample implementation, one call per round.
Public Class AsyncBarrier
Private ReadOnly _participantCount As Integer
Private _remainingParticipants As Integer
Private _tcs As TaskCompletionSource = New TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)
Public Sub New(participantCount As Integer)
If participantCount <= 0 Then Throw New ArgumentOutOfRangeException(NameOf(participantCount))
_participantCount = participantCount
_remainingParticipants = participantCount
End Sub
Public Function SignalAndWait() As Task
Dim tcs As TaskCompletionSource = _tcs
If Interlocked.Decrement(_remainingParticipants) = 0 Then
_remainingParticipants = _participantCount
_tcs = New TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)
tcs.SetResult()
End If
Return tcs.Task
End Function
End Class
Principais detalhes de implementação:
- Antes de concluir o
TaskCompletionSourcecompartilhado, o método redefine a contagem e troca por um novoTaskCompletionSourcepara a próxima rodada. Essa ordem garante que, quando os garçons retornarem, a barreira já esteja pronta para a próxima rodada. - Todos os participantes compartilham o mesmo
Task. Como o exemplo cria oTaskCompletionSourcecomRunContinuationsAsynchronously, as continuações são retomadas de forma assíncrona, em vez de serem executadas diretamente no thread que completa a barreira.
O exemplo a seguir executa três participantes em duas rodadas de uma barreira:
public static class AsyncBarrierDemo
{
public static async Task RunAsync()
{
var barrier = new AsyncBarrier(3);
int rounds = 2;
Task[] participants = Enumerable.Range(1, 3).Select(id => Task.Run(async () =>
{
for (int round = 1; round <= rounds; round++)
{
Console.WriteLine($"Participant {id}: working on round {round}");
await Task.Delay(id * 20);
Console.WriteLine($"Participant {id}: finished round {round}, waiting at barrier");
await barrier.SignalAndWait();
}
})).ToArray();
await Task.WhenAll(participants);
Console.WriteLine("All rounds complete.");
}
}
Public Module AsyncBarrierDemo
Public Async Function RunAsync() As Task
Dim barrier As New AsyncBarrier(3)
Dim rounds As Integer = 2
Dim participants As Task() = Enumerable.Range(1, 3).Select(
Function(id) Task.Run(Async Function()
For round As Integer = 1 To rounds
Console.WriteLine($"Participant {id}: working on round {round}")
Await Task.Delay(id * 20)
Console.WriteLine($"Participant {id}: finished round {round}, waiting at barrier")
Await barrier.SignalAndWait()
Next
End Function)).ToArray()
Await Task.WhenAll(participants)
Console.WriteLine("All rounds complete.")
End Function
End Module