Skip to content

Commit 32e7426

Browse files
BillWagnerCopilotgewarren
authored
Async coordination primitives (#53171)
* Async coordination primitives Fixes #17714 (final PR) 1. Create `async-coordination-primitives.md` — from "Building Async Coordination Primitives" parts 1-4 (AsyncManualResetEvent, AsyncAutoResetEvent, AsyncCountdownEvent, AsyncBarrier). 1. Create `async-coordination-primitives-advanced.md` — from parts 5-7 (AsyncSemaphore, AsyncLock, AsyncReaderWriterLock). **Note BCL equivalents** (`SemaphoreSlim.WaitAsync`, `System.Threading.Channels`). 1. **Heavy modernization needed:** update all code for current .NET idioms, call out which primitives now have framework equivalents. 1. Add both to TOC under a "Coordination primitives" sub-section. * Edit pass and address feedback. * fix build issues * Final content edits * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * respond to feedback. * Apply suggestions from code review Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> * Respond to feedback. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com>
1 parent 15e0a2f commit 32e7426

11 files changed

Lines changed: 1448 additions & 0 deletions

File tree

docs/navigate/advanced-programming/toc.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ items:
4040
href: ../../standard/asynchronous-programming-patterns/complete-your-tasks.md
4141
- name: Keep async methods alive
4242
href: ../../standard/asynchronous-programming-patterns/keep-async-methods-alive.md
43+
- name: Coordination primitives
44+
items:
45+
- name: Build async coordination primitives
46+
href: ../../standard/asynchronous-programming-patterns/async-coordination-primitives.md
47+
- name: Async semaphores, locks, and reader/writer coordination
48+
href: ../../standard/asynchronous-programming-patterns/async-coordination-primitives-advanced.md
4349
- name: Event-based asynchronous pattern (EAP)
4450
items:
4551
- name: Documentation overview
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
title: "Async semaphores, locks, and reader/writer coordination"
3+
description: Learn to use SemaphoreSlim.WaitAsync for async throttling, build async locks for mutual exclusion, and coordinate readers and writers in async code.
4+
ms.date: 04/16/2026
5+
ai-usage: ai-assisted
6+
dev_langs:
7+
- "csharp"
8+
- "vb"
9+
---
10+
11+
# Async semaphores, locks, and reader/writer coordination
12+
13+
When async code needs throttling, mutual exclusion, or reader/writer coordination, use the built-in .NET types rather than building your own. This article shows how to apply those types, and then walks through custom implementations to explain how they work internally.
14+
15+
## Async semaphore — throttle concurrent access
16+
17+
A semaphore limits how many callers can access a resource concurrently. <xref:System.Threading.SemaphoreSlim> provides a <xref:System.Threading.SemaphoreSlim.WaitAsync*> method that lets you await entry without blocking a thread:
18+
19+
:::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="SemaphoreSlimUsage":::
20+
:::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="SemaphoreSlimUsage":::
21+
22+
Always pair `WaitAsync` with `Release` in a `try`/`finally` block. If you forget to release, the semaphore count never increases, and other callers wait indefinitely.
23+
24+
### How an async semaphore works
25+
26+
Internally, an async semaphore maintains a count and a queue of waiters. When the count is above zero, `WaitAsync` decrements the count and returns immediately. When the count is zero, `WaitAsync` enqueues a <xref:System.Threading.Tasks.TaskCompletionSource> and returns its task. `Release` either dequeues a waiter and completes it, or increments the count:
27+
28+
:::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="AsyncSemaphore":::
29+
:::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="AsyncSemaphore":::
30+
31+
The `Release` method completes the `TaskCompletionSource` outside the lock, just like the `AsyncAutoResetEvent` in [Build async coordination primitives](async-coordination-primitives.md). This approach prevents synchronous continuations from running while the lock is held.
32+
33+
> [!NOTE]
34+
> `AsyncSemaphore` is an educational implementation. Use <xref:System.Threading.SemaphoreSlim> instead—it supports cancellation tokens, timeouts, and has been thoroughly tested.
35+
36+
## Async lock: mutual exclusion across awaits
37+
38+
A lock with a count of 1 provides mutual exclusion. The C# `lock` statement and <xref:System.Threading.Lock> (.NET 9+) don't work across `await` boundaries because they're thread-affine. A *thread-affine* lock the same thread that acquires the lock must be the one that releases it. Across an `await`, the thread that resumes the continuation might not be the thread that acquired the lock, which violates that requirement. Use <xref:System.Threading.SemaphoreSlim> with a count of 1 instead:
39+
40+
:::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="SemaphoreSlimAsLock":::
41+
:::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="SemaphoreSlimAsLock":::
42+
43+
### How an async lock works
44+
45+
You can wrap the semaphore pattern in a type that supports `using` for automatic release. The `LockAsync` method returns a disposable `Releaser`; when the `Releaser` is disposed, it releases the semaphore:
46+
47+
:::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="AsyncLock":::
48+
:::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="AsyncLock":::
49+
50+
Usage is concise and safe:
51+
52+
:::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="AsyncLockUsage":::
53+
:::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="AsyncLockUsage":::
54+
55+
> [!NOTE]
56+
> `AsyncLock` is an educational implementation. Use <xref:System.Threading.SemaphoreSlim> initialized to `1` with `try`/`finally` directly—the `AsyncLock` type shown here illustrates the disposable-releaser pattern but adds no capabilities beyond what `SemaphoreSlim` provides.
57+
58+
## Async reader/writer coordination
59+
60+
A reader/writer lock allows multiple concurrent readers but only one exclusive writer. .NET provides <xref:System.Threading.Tasks.ConcurrentExclusiveSchedulerPair>, which offers reader/writer scheduling for tasks through two <xref:System.Threading.Tasks.TaskScheduler> instances:
61+
62+
- <xref:System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentScheduler> — runs tasks concurrently (like readers), as long as no exclusive task is active.
63+
- <xref:System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ExclusiveScheduler> — runs tasks exclusively (like writers), with no other tasks running.
64+
65+
:::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="ConcurrentExclusiveUsage":::
66+
:::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="ConcurrentExclusiveUsage":::
67+
68+
> [!IMPORTANT]
69+
> `ConcurrentExclusiveSchedulerPair` protects at the task level, not across `await` boundaries. If a task queued to the `ExclusiveScheduler` contains an `await` on an incomplete operation, the exclusive lock releases when the `await` yields and reacquires when the continuation runs. Another exclusive or concurrent task can run during that gap. This behavior works well when you protect in-memory data structures and ensure no `await` interrupts the critical section. For scenarios that require holding the lock across awaits, use a custom `AsyncReaderWriterLock` like the one shown in the following section.
70+
71+
### Custom async reader/writer lock
72+
73+
The following implementation gives writers priority over readers. When a writer is waiting, new readers queue behind it. When a writer finishes and no other writers are waiting, all queued readers run together:
74+
75+
:::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="AsyncReaderWriterLock":::
76+
:::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="AsyncReaderWriterLock":::
77+
78+
Usage follows the same disposable-releaser pattern as `AsyncLock`:
79+
80+
:::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="AsyncReaderWriterLockUsage":::
81+
:::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="AsyncReaderWriterLockUsage":::
82+
83+
> [!TIP]
84+
> A production reader/writer lock requires thorough testing for edge cases: reentrancy, error paths, cancellation, and fairness policies. Consider established libraries (such as [Nito.AsyncEx](https://github.com/StephenCleary/AsyncEx)) before building your own.
85+
86+
## Channels as an alternative coordination pattern
87+
88+
<xref:System.Threading.Channels.Channel`1> provides a thread-safe producer-consumer queue that supports `async` reads and writes. Bounded channels (<xref:System.Threading.Channels.Channel.CreateBounded*>) provide natural back-pressure, replacing some scenarios where you'd otherwise use a semaphore for throttling.
89+
90+
For more information, see [System.Threading.Channels](/dotnet/core/extensions/channels).
91+
92+
## See also
93+
94+
- [Build async coordination primitives](async-coordination-primitives.md)
95+
- [Keep async methods alive](keep-async-methods-alive.md)
96+
- [Complete your tasks](complete-your-tasks.md)
97+
- [Consuming the Task-based Asynchronous Pattern](consuming-the-task-based-asynchronous-pattern.md)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
</Project>

0 commit comments

Comments
 (0)