ManualResetEventSlim、CountdownEvent、Barrierなどの同期調整プリミティブは、待機中に呼び出し元スレッドをブロックします。 非同期コードでは、スレッドをブロックすると、他の作業を行っている可能性のあるリソースが無駄になります。
TaskCompletionSourceを使用して、呼び出し元がブロックする代わりにawaitできる非同期の代替手段を構築します。
TaskCompletionSource は、SetResult()、SetException または SetCanceled を呼び出すことで手動で完了する Task を生成します。 そのタスクを待機するコードは、スレッドをブロックせずに中断し、ソースの完了時に再開します。 このパターンは、この記事のすべてのプリミティブの構成要素を形成します。
注
この記事のプリミティブは、教育的な実装です。 運用環境のスロットリングと相互除外の場合、「非同期セマフォ、ロック、およびおよびリーダー/ライターの調整」で説明されている組み込みの型を使用します。 作成するすべての TaskCompletionSource を常に完了します。ガイダンスについては、「 タスクを完了 する」を参照してください。
非同期の手動リセット イベント
手動リセット イベントは、非シグナル状態で開始されます。 呼び出し側はイベントを待機し、別のパーティがイベントをシグナルを送信 (設定) すると、すべての待機者が再開します。 イベントは、明示的にリセットするまで通知された状態を維持します。 同期に相当するものは ManualResetEventSlim。 .NET ランタイムは、TaskCompletionSource<TResult>を 1 ショット ブロードキャスト シグナリングに直接提供します。 リセット ラッパーを構築するのではなく、サイクルごとに新しいインスタンスを作成します。
TaskCompletionSource はそれ自体が 1 回限り手動リセット イベントです。 Task は、 Set* メソッドを呼び出してから、すべての awaiters が再開するまで不完全です。 新しいResetでスワップするTaskCompletionSourceメソッドを追加すると、再利用可能な非同期手動リセット イベントが発生します。
// 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
主要な実装の詳細:
- コンストラクターは TaskCreationOptions.RunContinuationsAsynchronously を渡して、
Setが呼び出し元のスレッドで待機者の継続を同期的に実行しないようにします。 このフラグがないと、Setが予期しない時間ブロックされる可能性があります。 -
Resetは、現在のTaskCompletionSourceが既に完了している場合にのみ、CompareExchange を使用して新しいTaskCompletionSourceと切り替えます。 このアトミック スワップにより、待機者が既に受信したタスクが孤立化するのを防ぎます。
次の例は、2 つのタスクがイベントを通じてどのように連携するかを示しています。
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
非同期自動リセット イベント
自動リセット イベントは手動リセット イベントに似ていますが、1 つの待機者を解放すると、自動的に非シグナル状態に戻ります。 イベントが通知されたときに複数の呼び出し側が待機している場合は、1 つの待機者のみが再開します。 同期に相当するものは AutoResetEvent。 .NET ランタイムには、シングル ウェイター非同期シグナリング用の SemaphoreSlim が含まれています。
0を最大1に設定して初期化し、WaitAsyncを呼び出して待機し、Releaseでシグナルを送ります。
各シグナルは待機者を 1 つだけ解放するため、個々に完了できるように、 TaskCompletionSource インスタンスのコレクション (待機者ごとに 1 つ) が必要です。
// 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
主要な実装の詳細:
-
Setメソッドは、ロックTaskCompletionSourceにある (TCS) を完了します。 ロック内で TCS を完了すると、ロックが保持されている間に同期継続が実行され、デッドロックや予期しない再入が発生する可能性があります。 -
Setが呼び出され、待機者がキューに登録されていない場合は、次のWaitAsync呼び出しがすぐに完了するようにシグナルが格納されます。
次の例は、イベントを通じてコンシューマーに通知するプロデューサーを示しています。
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
非同期カウントダウン イベント
カウントダウン イベントは、待機者が続行できるようになる前に、指定された数のシグナルを待機します。 このパターンは、N 個の操作を開始し、すべての N 個の完了を待機するフォーク/結合シナリオに役立ちます。 同期に相当するものは CountdownEvent。 .NET ランタイムは、固定された一連のタスクとのフォーク/結合の調整にWhenAllを提供します。 代わりに使用してください。
前のセクションの AsyncManualResetEvent をアトミック カウンターで構成して、非同期バージョンをビルドします。
// 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
Signalメソッドは、Decrementを使用してカウントをアトミックにデクリメントします。 カウントが 0 に達すると、内部イベントが設定され、すべての待機中のスレッドが実行を再開します。
次の例では、カウントダウン イベントを使用して、3 つの同時操作を待機します。
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
非同期バリア
バリアは、複数のラウンドにわたって固定された参加者のセットを調整します。 各参加者は、現在のラウンドの作業が完了したときに通知し、他のすべての参加者が完了するまで待機します。 最後の参加者が信号を出すと、すべての参加者が再開し、次のラウンドのためにバリアがリセットされます。 同期に相当するものは Barrier。 .NET ランタイムは、マルチラウンド非同期同期WhenAllを提供します。 ループと組み合わせて、ラウンドごとに 1 つの WhenAll 呼び出しを行います。
// 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
主要な実装の詳細:
- 共有
TaskCompletionSourceを完了する前に、メソッドはカウントをリセットし、次のラウンドの新しいTaskCompletionSourceでスワップします。 この順序により、待機者が再開すると、バリアは次のラウンドに向けて既に準備が整います。 - すべての参加者が同じ
Taskを共有します。 このサンプルでは、RunContinuationsAsynchronouslyを使用してTaskCompletionSourceを作成するため、バリアを完了するスレッドでインラインで実行するのではなく、継続が非同期的に再開されます。
次の例では、3 人の参加者が 2 ラウンドのバリアを通過します。
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
こちらも参照ください
.NET