Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/navigate/advanced-programming/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ items:
href: ../../standard/asynchronous-programming-patterns/common-async-bugs.md
- name: Async lambda pitfalls
href: ../../standard/asynchronous-programming-patterns/async-lambda-pitfalls.md
- name: Coordination primitives
items:
- name: Build async coordination primitives
href: ../../standard/asynchronous-programming-patterns/async-coordination-primitives.md
- name: Async semaphores, locks, and reader/writer coordination
href: ../../standard/asynchronous-programming-patterns/async-coordination-primitives-advanced.md
- name: Event-based asynchronous pattern (EAP)
items:
- name: Documentation overview
Expand Down
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:

:::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.

> [!TIP]
> In production code, use <xref:System.Threading.SemaphoreSlim> instead of this custom type. `SemaphoreSlim.WaitAsync` 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:

:::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":::

> [!TIP]
> In production code, use `SemaphoreSlim(1, 1)` directly with `try`/`finally`. The custom `AsyncLock` type shown here illustrates the disposable-releaser pattern but adds complexity without adding 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.

:::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.

For more information, see [System.Threading.Channels](/dotnet/core/extensions/channels).

## See also

- [Build async coordination primitives](async-coordination-primitives.md)
- [Keeping async methods alive](keeping-async-methods-alive.md)
- [Complete your tasks](complete-your-tasks.md)
- [Consuming the Task-based Asynchronous Pattern](consuming-the-task-based-asynchronous-pattern.md)
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.

> [!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>.

`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.

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>.

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.
- 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>.

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.

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>.

:::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.

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)
- [Keeping async methods alive](keeping-async-methods-alive.md)
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>
Loading
Loading