-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Async coordination primitives #53171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
e99d600
Async coordination primitives
BillWagner 6904e98
Edit pass and address feedback.
BillWagner 7ed1394
fix build issues
BillWagner 4467134
Final content edits
BillWagner 5d461ad
Apply suggestions from code review
BillWagner 653d9a4
respond to feedback.
BillWagner 29a2ada
Merge branch 'async-coordination' of https://github.com/BillWagner/do…
BillWagner 6ae0e73
Apply suggestions from code review
BillWagner 72f9974
Respond to feedback.
BillWagner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
...ard/asynchronous-programming-patterns/async-coordination-primitives-advanced.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| --- | ||
| title: "Async semaphores, locks, and reader/writer coordination" | ||
| description: Learn to use SemaphoreSlim.WaitAsync for async throttling, build async locks for mutual exclusion, and coordinate readers and writers in async code. | ||
| ms.date: 04/16/2026 | ||
| ai-usage: ai-assisted | ||
| dev_langs: | ||
| - "csharp" | ||
| - "vb" | ||
| helpviewer_keywords: | ||
| - "SemaphoreSlim.WaitAsync" | ||
| - "async lock" | ||
| - "async semaphore" | ||
| - "AsyncLock" | ||
| - "reader/writer lock, async" | ||
| - "ConcurrentExclusiveSchedulerPair" | ||
| - "System.Threading.Channels" | ||
| --- | ||
|
|
||
| # Async semaphores, locks, and reader/writer coordination | ||
|
|
||
| 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. | ||
|
|
||
| ## Async semaphore — throttle concurrent access | ||
|
|
||
| A semaphore limits how many callers can access a resource concurrently. <xref:System.Threading.SemaphoreSlim> provides a <xref:System.Threading.SemaphoreSlim.WaitAsync%2A> method that lets you await entry without blocking a thread: | ||
|
BillWagner marked this conversation as resolved.
Outdated
|
||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="SemaphoreSlimUsage"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="SemaphoreSlimUsage"::: | ||
|
|
||
| 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. | ||
|
|
||
| ### How an async semaphore works | ||
|
|
||
| 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: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="AsyncSemaphore"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="AsyncSemaphore"::: | ||
|
|
||
| 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. | ||
|
|
||
| > [!NOTE] | ||
| > `AsyncSemaphore` is an educational implementation. Use <xref:System.Threading.SemaphoreSlim> instead—it supports cancellation tokens, timeouts, and has been thoroughly tested. | ||
|
|
||
| ## Async lock: mutual exclusion across awaits | ||
|
|
||
| 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. The thread that acquires the lock might not be the thread that resumes after the `await`. Use <xref:System.Threading.SemaphoreSlim> with a count of 1 instead: | ||
|
BillWagner marked this conversation as resolved.
Outdated
|
||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="SemaphoreSlimAsLock"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="SemaphoreSlimAsLock"::: | ||
|
|
||
| ### How an async lock works | ||
|
|
||
| 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: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="AsyncLock"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="AsyncLock"::: | ||
|
|
||
| Usage is concise and safe: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="AsyncLockUsage"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="AsyncLockUsage"::: | ||
|
|
||
| > [!NOTE] | ||
| > `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. | ||
|
|
||
| ## Async reader/writer coordination | ||
|
|
||
| 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: | ||
|
|
||
| - <xref:System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentScheduler%2A> — runs tasks concurrently (like readers), as long as no exclusive task is active. | ||
| - <xref:System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ExclusiveScheduler%2A> — runs tasks exclusively (like writers), with no other tasks running. | ||
|
BillWagner marked this conversation as resolved.
Outdated
|
||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="ConcurrentExclusiveUsage"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="ConcurrentExclusiveUsage"::: | ||
|
|
||
| > [!IMPORTANT] | ||
| > `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. | ||
|
|
||
| ### Custom async reader/writer lock | ||
|
|
||
| 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: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="AsyncReaderWriterLock"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="AsyncReaderWriterLock"::: | ||
|
|
||
| Usage follows the same disposable-releaser pattern as `AsyncLock`: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives-advanced/csharp/Program.cs" id="AsyncReaderWriterLockUsage"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives-advanced/vb/Program.vb" id="AsyncReaderWriterLockUsage"::: | ||
|
|
||
| > [!TIP] | ||
| > 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. | ||
|
|
||
| ## Channels as an alternative coordination pattern | ||
|
|
||
| <xref:System.Threading.Channels.Channel%601> provides a thread-safe producer-consumer queue that supports `async` reads and writes. Bounded channels (<xref:System.Threading.Channels.Channel.CreateBounded%2A>) provide natural back-pressure, replacing some scenarios where you'd otherwise use a semaphore for throttling. | ||
|
BillWagner marked this conversation as resolved.
Outdated
|
||
|
|
||
| For more information, see [System.Threading.Channels](/dotnet/core/extensions/channels). | ||
|
|
||
| ## See also | ||
|
|
||
| - [Build async coordination primitives](async-coordination-primitives.md) | ||
| - [Keep async methods alive](keep-async-methods-alive.md) | ||
| - [Complete your tasks](complete-your-tasks.md) | ||
| - [Consuming the Task-based Asynchronous Pattern](consuming-the-task-based-asynchronous-pattern.md) | ||
104 changes: 104 additions & 0 deletions
104
docs/standard/asynchronous-programming-patterns/async-coordination-primitives.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| --- | ||
| title: "Build async coordination primitives" | ||
| description: Learn how to build async coordination primitives using TaskCompletionSource, including manual-reset events, auto-reset events, countdown events, and barriers. | ||
| ms.date: 04/16/2026 | ||
| ai-usage: ai-assisted | ||
| dev_langs: | ||
| - "csharp" | ||
| - "vb" | ||
| helpviewer_keywords: | ||
| - "async coordination" | ||
| - "TaskCompletionSource" | ||
| - "AsyncManualResetEvent" | ||
| - "AsyncAutoResetEvent" | ||
| - "AsyncCountdownEvent" | ||
| - "AsyncBarrier" | ||
| - "coordination primitives" | ||
| --- | ||
|
|
||
| # Build async coordination primitives | ||
|
|
||
| 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. | ||
|
|
||
| A `TaskCompletionSource` produces a <xref:System.Threading.Tasks.Task> that you complete manually by calling <xref:System.Threading.Tasks.TaskCompletionSource.SetResult%2A>, <xref:System.Threading.Tasks.TaskCompletionSource.SetException%2A>, or <xref:System.Threading.Tasks.TaskCompletionSource.SetCanceled%2A>. 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. | ||
|
BillWagner marked this conversation as resolved.
Outdated
|
||
|
|
||
| > [!NOTE] | ||
| > 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. | ||
|
|
||
| ## Async manual-reset event | ||
|
|
||
| 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%601> directly for one-shot broadcast signaling—create a new instance each cycle rather than building a reset wrapper around it. | ||
|
BillWagner marked this conversation as resolved.
Outdated
|
||
|
|
||
| `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. | ||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncManualResetEvent"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncManualResetEvent"::: | ||
|
|
||
| Key implementation details: | ||
|
|
||
| - 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. | ||
| - `Reset` uses <xref:System.Threading.Interlocked.CompareExchange%2A> 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. | ||
|
BillWagner marked this conversation as resolved.
Outdated
|
||
|
|
||
| The following example shows how two tasks coordinate through the event: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncManualResetEventUsage"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncManualResetEventUsage"::: | ||
|
|
||
| ## Async auto-reset event | ||
|
|
||
| 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. | ||
|
|
||
| Because each signal releases only one waiter, you need a collection of `TaskCompletionSource` instances—one per waiter—so you can complete them individually: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncAutoResetEvent"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncAutoResetEvent"::: | ||
|
|
||
| Key implementation details: | ||
|
|
||
| - The `Set` method completes the `TaskCompletionSource` *outside* the lock. Completing a TCS inside the lock runs synchronous continuations while the lock is held, which could cause deadlocks or unexpected reentrancy. | ||
|
BillWagner marked this conversation as resolved.
Outdated
|
||
| - When `Set` is called and no waiter is queued, the signal is stored so the next `WaitAsync` call completes immediately. | ||
|
|
||
| The following example shows a producer signaling a consumer through the event: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncAutoResetEventUsage"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncAutoResetEventUsage"::: | ||
|
|
||
| ## Async countdown event | ||
|
|
||
| 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%2A> for fork/join coordination with a fixed set of tasks. Use it instead. | ||
|
BillWagner marked this conversation as resolved.
Outdated
|
||
|
|
||
| Build the async version by composing the `AsyncManualResetEvent` from the previous section with an atomic counter: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncCountdownEvent"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncCountdownEvent"::: | ||
|
|
||
| The `Signal` method decrements the count atomically with <xref:System.Threading.Interlocked.Decrement%2A>. When the count reaches zero, it sets the inner event, and all waiters resume. | ||
|
BillWagner marked this conversation as resolved.
Outdated
|
||
|
|
||
| The following example uses a countdown event to await three concurrent operations: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncCountdownEventUsage"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncCountdownEventUsage"::: | ||
|
|
||
| ## Async barrier | ||
|
|
||
| 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%2A> for multi-round async synchronization. Combine it with a loop, one `WhenAll` call per round. | ||
|
BillWagner marked this conversation as resolved.
Outdated
|
||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncBarrier"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncBarrier"::: | ||
|
|
||
| Key implementation details: | ||
|
|
||
| - 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. | ||
| - All participants share the same `Task`, which means all synchronous continuations run in series on the thread that completes the task. If that serialization is a concern, give each participant its own `TaskCompletionSource` and complete them in parallel. | ||
|
BillWagner marked this conversation as resolved.
Outdated
|
||
|
|
||
| The following example runs three participants through two rounds of a barrier: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-coordination-primitives/csharp/Program.cs" id="AsyncBarrierUsage"::: | ||
| :::code language="vb" source="./snippets/async-coordination-primitives/vb/Program.vb" id="AsyncBarrierUsage"::: | ||
|
|
||
| ## See also | ||
|
|
||
| - [Async semaphores, locks, and reader/writer coordination](async-coordination-primitives-advanced.md) | ||
| - [Task-based asynchronous pattern (TAP)](task-based-asynchronous-pattern-tap.md) | ||
| - [Complete your tasks](complete-your-tasks.md) | ||
| - [Keep async methods alive](keep-async-methods-alive.md) | ||
10 changes: 10 additions & 0 deletions
10
.../async-coordination-primitives-advanced/csharp/AsyncCoordinationPrimitivesAdvanced.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| </PropertyGroup> | ||
|
|
||
| </Project> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.