|
| 1 | +--- |
| 2 | +title: "Build async coordination primitives" |
| 3 | +description: Learn how to build async coordination primitives using TaskCompletionSource, including manual-reset events, auto-reset events, countdown events, and barriers. |
| 4 | +ms.date: 04/16/2026 |
| 5 | +ai-usage: ai-assisted |
| 6 | +dev_langs: |
| 7 | + - "csharp" |
| 8 | + - "vb" |
| 9 | +helpviewer_keywords: |
| 10 | + - "async coordination" |
| 11 | + - "TaskCompletionSource" |
| 12 | + - "AsyncManualResetEvent" |
| 13 | + - "AsyncAutoResetEvent" |
| 14 | + - "AsyncCountdownEvent" |
| 15 | + - "AsyncBarrier" |
| 16 | + - "coordination primitives" |
| 17 | +--- |
| 18 | + |
| 19 | +# Build async coordination primitives |
| 20 | + |
| 21 | +Synchronous coordination primitives like <xref:System.Threading.ManualResetEventSlim>, <xref:System.Threading.CountdownEvent>, and <xref:System.Threading.Barrier> block the calling thread while waiting. In async code, blocking a thread wastes a resource that could be doing other work. Use <xref:System.Threading.Tasks.TaskCompletionSource> to build async equivalents that let callers `await` instead of blocking. |
| 22 | + |
| 23 | +A `TaskCompletionSource` produces a <xref:System.Threading.Tasks.Task> that you complete manually by calling <xref:System.Threading.Tasks.TaskCompletionSource.SetResult>, <xref:System.Threading.Tasks.TaskCompletionSource.SetException*>, or <xref:System.Threading.Tasks.TaskCompletionSource.SetCanceled*>. Code that awaits that task suspends without blocking a thread and resumes when you complete the source. This pattern forms the building block for every primitive in this article. |
| 24 | + |
| 25 | +> [!NOTE] |
| 26 | +> The primitives in this article are educational implementations. For production throttling and mutual exclusion, use the built-in types covered in [Async semaphores, locks, and reader/writer coordination](async-coordination-primitives-advanced.md). Always complete every `TaskCompletionSource` you create; see [Complete your tasks](complete-your-tasks.md) for guidance. |
| 27 | +
|
| 28 | +## Async manual-reset event |
| 29 | + |
| 30 | +A manual-reset event starts in a non-signaled state. Callers wait for the event, and all waiters resume when another party signals (sets) the event. The event stays signaled until you explicitly reset it. The synchronous equivalent is <xref:System.Threading.ManualResetEventSlim>. The .NET runtime provides <xref:System.Threading.Tasks.TaskCompletionSource`1> directly for one-shot broadcast signaling. Create a new instance each cycle rather than building a reset wrapper around it. |
| 31 | + |
| 32 | +`TaskCompletionSource` is itself a one-shot manual-reset event: its `Task` is incomplete until you call a `Set*` method, and then all awaiters resume. Add a `Reset` method that swaps in a new `TaskCompletionSource`, and you have a reusable async manual-reset event. |
| 33 | + |
| 34 | +:::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncManualResetEvent"::: |
| 35 | +:::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncManualResetEvent"::: |
| 36 | + |
| 37 | +Key implementation details: |
| 38 | + |
| 39 | +- The constructor passes <xref:System.Threading.Tasks.TaskCreationOptions.RunContinuationsAsynchronously?displayProperty=nameWithType> to prevent `Set` from running waiter continuations synchronously on the calling thread. Without this flag, `Set` could block for an unpredictable amount of time. |
| 40 | +- `Reset` uses <xref:System.Threading.Interlocked.CompareExchange*> to swap in a new `TaskCompletionSource` only when the current one is already completed. This atomic swap prevents orphaning a task that a waiter already received. |
| 41 | + |
| 42 | +The following example shows how two tasks coordinate through the event: |
| 43 | + |
| 44 | +:::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncManualResetEventUsage"::: |
| 45 | +:::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncManualResetEventUsage"::: |
| 46 | + |
| 47 | +## Async auto-reset event |
| 48 | + |
| 49 | +An auto-reset event is similar to a manual-reset event, but it automatically returns to the non-signaled state after releasing exactly one waiter. If multiple callers are waiting when the event is signaled, only one waiter resumes. The synchronous equivalent is <xref:System.Threading.AutoResetEvent>. The .NET runtime includes <xref:System.Threading.SemaphoreSlim> for single-waiter async signaling. Initialize it to `0` with a maximum count of `1` and call `WaitAsync` to wait and `Release` to signal. |
| 50 | + |
| 51 | +Because each signal releases only one waiter, you need a collection of `TaskCompletionSource` instances—one per waiter—so you can complete them individually: |
| 52 | + |
| 53 | +:::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncAutoResetEvent"::: |
| 54 | +:::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncAutoResetEvent"::: |
| 55 | + |
| 56 | +Key implementation details: |
| 57 | + |
| 58 | +- The `Set` method completes the `TaskCompletionSource` (TCS) *outside* the lock. Completing a TCS inside the lock runs synchronous continuations while the lock is held, which could cause deadlocks or unexpected reentrancy. |
| 59 | +- When `Set` is called and no waiter is queued, the signal is stored so the next `WaitAsync` call completes immediately. |
| 60 | + |
| 61 | +The following example shows a producer signaling a consumer through the event: |
| 62 | + |
| 63 | +:::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncAutoResetEventUsage"::: |
| 64 | +:::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncAutoResetEventUsage"::: |
| 65 | + |
| 66 | +## Async countdown event |
| 67 | + |
| 68 | +A countdown event waits for a specified number of signals before it allows waiters to proceed. This pattern is useful for fork/join scenarios where you start N operations and want to await all N completions. The synchronous equivalent is <xref:System.Threading.CountdownEvent>. The .NET runtime provides <xref:System.Threading.Tasks.Task.WhenAll*> for fork/join coordination with a fixed set of tasks. Use it instead. |
| 69 | + |
| 70 | +Build the async version by composing the `AsyncManualResetEvent` from the previous section with an atomic counter: |
| 71 | + |
| 72 | +:::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncCountdownEvent"::: |
| 73 | +:::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncCountdownEvent"::: |
| 74 | + |
| 75 | +The `Signal` method decrements the count atomically with <xref:System.Threading.Interlocked.Decrement*>. When the count reaches zero, it sets the inner event, and all waiters resume. |
| 76 | + |
| 77 | +The following example uses a countdown event to await three concurrent operations: |
| 78 | + |
| 79 | +:::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncCountdownEventUsage"::: |
| 80 | +:::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncCountdownEventUsage"::: |
| 81 | + |
| 82 | +## Async barrier |
| 83 | + |
| 84 | +A barrier coordinates a fixed set of participants across multiple rounds. Each participant signals when it finishes its work for the current round and then waits for all other participants to finish. When the last participant signals, all participants resume, and the barrier resets for the next round. The synchronous equivalent is <xref:System.Threading.Barrier>. The .NET runtime provides <xref:System.Threading.Tasks.Task.WhenAll*> for multi-round async synchronization. Combine it with a loop, one `WhenAll` call per round. |
| 85 | + |
| 86 | +:::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncBarrier"::: |
| 87 | +:::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncBarrier"::: |
| 88 | + |
| 89 | +Key implementation details: |
| 90 | + |
| 91 | +- Before completing the shared `TaskCompletionSource`, the method resets the count and swaps in a new `TaskCompletionSource` for the next round. This ordering ensures that when waiters resume, the barrier is already ready for the next round. |
| 92 | +- All participants share the same `Task`. Because the sample creates the `TaskCompletionSource` with `RunContinuationsAsynchronously`, continuations resume asynchronously instead of running inline on the thread that completes the barrier. |
| 93 | + |
| 94 | +The following example runs three participants through two rounds of a barrier: |
| 95 | + |
| 96 | +:::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncBarrierUsage"::: |
| 97 | +:::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncBarrierUsage"::: |
| 98 | + |
| 99 | +## See also |
| 100 | + |
| 101 | +- [Async semaphores, locks, and reader/writer coordination](async-coordination-primitives-advanced.md) |
| 102 | +- [Task-based asynchronous pattern (TAP)](task-based-asynchronous-pattern-tap.md) |
| 103 | +- [Complete your tasks](complete-your-tasks.md) |
| 104 | +- [Keep async methods alive](keep-async-methods-alive.md) |
0 commit comments