-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Sync/Async wrapper article #52879
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
Sync/Async wrapper article #52879
Changes from 5 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
f15c26d
Sync/Async wrapper article
BillWagner eeb8206
proofread and edit.
BillWagner 697150d
Apply suggestions from code review
BillWagner 11058f6
respond to feedback.
BillWagner ffdc355
Merge branch 'async-aweigh-1' of https://github.com/BillWagner/docs i…
BillWagner a5a6ca2
Apply suggestions from code review
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
93 changes: 93 additions & 0 deletions
93
...ard/asynchronous-programming-patterns/async-wrappers-for-synchronous-methods.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,93 @@ | ||
| --- | ||
| title: "Asynchronous wrappers for synchronous methods" | ||
| description: Learn why you should avoid exposing asynchronous wrappers for synchronous methods in .NET libraries, and when consumers should use Task.Run instead. | ||
| ms.date: 04/06/2026 | ||
| ai-usage: ai-assisted | ||
| dev_langs: | ||
| - "csharp" | ||
| - "vb" | ||
| helpviewer_keywords: | ||
| - "async over sync" | ||
| - "Task.Run wrapper" | ||
| - "asynchronous programming, wrappers" | ||
| - "TAP, async wrappers" | ||
| --- | ||
| # Asynchronous wrappers for synchronous methods | ||
|
|
||
| When you have a synchronous method in a library, you might be tempted to expose an asynchronous counterpart that wraps it in <xref:System.Threading.Tasks.Task.Run*?displayProperty=nameWithType>: | ||
|
|
||
| ```csharp | ||
| public T Foo() { /* synchronous work */ } | ||
|
|
||
| // Don't do this in a library: | ||
| public Task<T> FooAsync() | ||
| { | ||
| return Task.Run(() => Foo()); | ||
| } | ||
| ``` | ||
|
|
||
| This article explains why that approach is almost always wrong for libraries and how to think about the tradeoffs. | ||
|
|
||
| ## Scalability vs. offloading | ||
|
|
||
| Asynchronous programming provides two distinct benefits: | ||
|
|
||
| - **Scalability** — Reduce resource consumption by freeing threads during I/O waits. | ||
| - **Offloading** — Move work to a different thread to maintain responsiveness (for example, keeping a UI thread free) or achieve parallelism. | ||
|
|
||
| These benefits require different approaches. The critical distinction: **wrapping a synchronous method in `Task.Run` helps with offloading but does nothing for scalability.** | ||
|
|
||
| ### Why `Task.Run` doesn't improve scalability | ||
|
|
||
| A truly asynchronous implementation reduces the number of threads consumed during a long-running operation. A `Task.Run` wrapper still blocks a thread — it just moves the blocking from one thread to another: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-wrappers-for-synchronous-methods/csharp/Program.cs" id="ScalabilityWrong"::: | ||
| :::code language="vb" source="./snippets/async-wrappers-for-synchronous-methods/vb/Program.vb" id="ScalabilityWrong"::: | ||
|
|
||
| Compare that approach with a truly asynchronous implementation that consumes no threads while waiting: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-wrappers-for-synchronous-methods/csharp/Program.cs" id="ScalabilityRight"::: | ||
| :::code language="vb" source="./snippets/async-wrappers-for-synchronous-methods/vb/Program.vb" id="ScalabilityRight"::: | ||
|
|
||
| Both implementations complete after the specified delay, but the second implementation doesn't block any thread while waiting. For server applications handling many concurrent requests, that difference directly affects how many requests a server can process simultaneously. | ||
|
|
||
| ### Offloading is the consumer's responsibility | ||
|
|
||
| Wrapping synchronous calls in `Task.Run` is useful for offloading work from a UI thread. However, the consumer, not the library, should handle this wrapping: | ||
|
|
||
| :::code language="csharp" source="./snippets/async-wrappers-for-synchronous-methods/csharp/Program.cs" id="OffloadFromUI"::: | ||
| :::code language="vb" source="./snippets/async-wrappers-for-synchronous-methods/vb/Program.vb" id="OffloadFromUI"::: | ||
|
|
||
| The consumer knows their context: whether they're on a UI thread, how much granularity they need, and whether offloading adds value. The library doesn't. | ||
|
|
||
| ## Why libraries shouldn't expose async-over-sync wrappers | ||
|
|
||
| When a library exposes only the synchronous method (and not an async wrapper), consumers benefit in several ways: | ||
|
|
||
| - **Reduced API surface area**: Fewer methods to learn, test, and maintain. | ||
| - **No misleading scalability expectations**: Users know that only the methods exposed as asynchronous actually provide scalability benefits. | ||
| - **Consumer control**: Callers choose *whether* and *how* to offload, at the right level of granularity. A high-throughput server application can call the synchronous method directly, avoiding unnecessary overhead from `Task.Run`. | ||
| - **Better performance**: Asynchronous wrappers add overhead through allocations, context switches, and thread pool scheduling. For fine-grained operations, that overhead can be significant. | ||
|
|
||
| ## Exceptions: when async-over-sync wrappers make sense | ||
|
|
||
| Some base classes expose asynchronous methods so that derived classes can override them with truly asynchronous implementations. The base class provides an async-over-sync default. | ||
|
|
||
| For example, <xref:System.IO.Stream> exposes <xref:System.IO.Stream.ReadAsync*> and <xref:System.IO.Stream.WriteAsync*>. The base implementations wrap the synchronous <xref:System.IO.Stream.Read*> and <xref:System.IO.Stream.Write*> methods. Derived classes like <xref:System.IO.FileStream> and <xref:System.Net.Sockets.NetworkStream> override these methods with asynchronous I/O implementations that provide real scalability benefits. | ||
|
|
||
| Similarly, <xref:System.IO.TextReader> provides <xref:System.IO.TextReader.ReadToEndAsync*> on the base class as a wrapper, and <xref:System.IO.StreamReader> overrides it with a truly asynchronous implementation that calls <xref:System.IO.Stream.ReadAsync*> internally. | ||
|
|
||
| These exceptions are valid because: | ||
|
|
||
| - The pattern is designed for polymorphism. Callers interact with the base type. | ||
| - Derived types provide truly asynchronous overrides. | ||
|
|
||
| ## Guideline | ||
|
|
||
| Expose asynchronous methods from a library only when the implementation provides real scalability benefits over its synchronous counterpart. Don't expose asynchronous methods purely for offloading. Leave that choice to the consumer. | ||
|
|
||
| ## See also | ||
|
|
||
| - [Task-based asynchronous pattern (TAP)](task-based-asynchronous-pattern-tap.md) | ||
| - [Implement the task-based asynchronous pattern](implementing-the-task-based-asynchronous-pattern.md) | ||
| - [Synchronous wrappers for asynchronous methods](synchronous-wrappers-for-asynchronous-methods.md) | ||
10 changes: 10 additions & 0 deletions
10
...tterns/snippets/async-wrappers-for-synchronous-methods/csharp/AsyncWrappersForSync.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> |
63 changes: 63 additions & 0 deletions
63
...us-programming-patterns/snippets/async-wrappers-for-synchronous-methods/csharp/Program.cs
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,63 @@ | ||
| // <ScalabilityWrong> | ||
| public static class TimerExampleWrong | ||
| { | ||
| public static Task SleepAsync(int millisecondsTimeout) | ||
| { | ||
| return Task.Run(() => Thread.Sleep(millisecondsTimeout)); | ||
| } | ||
| } | ||
| // </ScalabilityWrong> | ||
|
|
||
| // <ScalabilityRight> | ||
| public static class TimerExampleRight | ||
| { | ||
| public static Task SleepAsync(int millisecondsTimeout) | ||
| { | ||
| var tcs = new TaskCompletionSource<bool>(); | ||
| var timer = new Timer( | ||
| _ => tcs.TrySetResult(true), null, millisecondsTimeout, Timeout.Infinite); | ||
|
|
||
| tcs.Task.ContinueWith( | ||
| _ => timer.Dispose(), TaskScheduler.Default); | ||
|
|
||
| return tcs.Task; | ||
| } | ||
| } | ||
| // </ScalabilityRight> | ||
|
|
||
| // <OffloadFromUI> | ||
| public static class UIOffloadExample | ||
| { | ||
| public static int ComputeIntensive(int input) | ||
| { | ||
| int result = 0; | ||
| for (int i = 0; i < input; i++) | ||
| { | ||
| result += i; | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| public static async Task ConsumeFromUIThreadAsync() | ||
| { | ||
| int result = await Task.Run(() => ComputeIntensive(10_000)); | ||
| Console.WriteLine($"Result: {result}"); | ||
| } | ||
| } | ||
| // </OffloadFromUI> | ||
|
|
||
| // Verification entry point | ||
| public class Program | ||
| { | ||
| public static async Task Main() | ||
| { | ||
| Console.WriteLine("--- ScalabilityRight demo ---"); | ||
| await TimerExampleRight.SleepAsync(100); | ||
| Console.WriteLine("SleepAsync completed (100ms)."); | ||
|
|
||
| Console.WriteLine("--- OffloadFromUI demo ---"); | ||
| await UIOffloadExample.ConsumeFromUIThreadAsync(); | ||
|
|
||
| Console.WriteLine("Done."); | ||
| } | ||
| } |
9 changes: 9 additions & 0 deletions
9
...g-patterns/snippets/async-wrappers-for-synchronous-methods/vb/AsyncWrappersForSync.vbproj
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,9 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <RootNamespace>AsyncWrappersForSync</RootNamespace> | ||
| </PropertyGroup> | ||
|
|
||
| </Project> |
54 changes: 54 additions & 0 deletions
54
...ronous-programming-patterns/snippets/async-wrappers-for-synchronous-methods/vb/Program.vb
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,54 @@ | ||
| Imports System.Threading | ||
|
|
||
| ' <ScalabilityWrong> | ||
| Public Module TimerExampleWrong | ||
| Public Function SleepAsync(millisecondsTimeout As Integer) As Task | ||
| Return Task.Run(Sub() Thread.Sleep(millisecondsTimeout)) | ||
| End Function | ||
| End Module | ||
| ' </ScalabilityWrong> | ||
|
|
||
| ' <ScalabilityRight> | ||
| Public Module TimerExampleRight | ||
| Public Function SleepAsync(millisecondsTimeout As Integer) As Task | ||
| Dim tcs As New TaskCompletionSource(Of Boolean)() | ||
| Dim tmr As New Timer( | ||
| Sub(state) tcs.TrySetResult(True), Nothing, millisecondsTimeout, Timeout.Infinite) | ||
|
|
||
| tcs.Task.ContinueWith( | ||
| Sub(t) tmr.Dispose(), TaskScheduler.Default) | ||
|
|
||
| Return tcs.Task | ||
| End Function | ||
| End Module | ||
| ' </ScalabilityRight> | ||
|
|
||
| ' <OffloadFromUI> | ||
| Public Module UIOffloadExample | ||
| Public Function ComputeIntensive(input As Integer) As Integer | ||
| Dim result As Integer = 0 | ||
| For i As Integer = 0 To input - 1 | ||
| result += i | ||
| Next | ||
| Return result | ||
| End Function | ||
|
|
||
| Public Async Function ConsumeFromUIThreadAsync() As Task | ||
| Dim result As Integer = Await Task.Run(Function() ComputeIntensive(10_000)) | ||
| Console.WriteLine($"Result: {result}") | ||
| End Function | ||
| End Module | ||
| ' </OffloadFromUI> | ||
|
|
||
| Module Program | ||
| Sub Main() | ||
| Console.WriteLine("--- ScalabilityRight demo ---") | ||
| TimerExampleRight.SleepAsync(100).Wait() | ||
| Console.WriteLine("SleepAsync completed (100ms).") | ||
|
|
||
| Console.WriteLine("--- OffloadFromUI demo ---") | ||
| UIOffloadExample.ConsumeFromUIThreadAsync().Wait() | ||
|
|
||
| Console.WriteLine("Done.") | ||
| End Sub | ||
| End Module |
76 changes: 76 additions & 0 deletions
76
...us-programming-patterns/snippets/synchronous-wrappers-for-async-methods/csharp/Program.cs
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,76 @@ | ||
| // <SyncOverAsyncTAP> | ||
| public class TapWrapper | ||
| { | ||
| public static int Foo(Func<Task<int>> fooAsync) | ||
| { | ||
| return fooAsync().Result; | ||
| } | ||
| } | ||
| // </SyncOverAsyncTAP> | ||
|
|
||
| // <DeadlockExample> | ||
| public static class DeadlockExample | ||
| { | ||
| private static void Delay(int milliseconds) | ||
| { | ||
| DelayAsync(milliseconds).Wait(); | ||
| } | ||
|
|
||
| private static async Task DelayAsync(int milliseconds) | ||
| { | ||
| await Task.Delay(milliseconds); | ||
| } | ||
| } | ||
| // </DeadlockExample> | ||
|
|
||
| // <ThreadPoolDeadlock> | ||
| public static class ThreadPoolDeadlockExample | ||
| { | ||
| public static int Foo(Func<Task<int>> fooAsync) | ||
| { | ||
| return fooAsync().Result; | ||
| } | ||
|
|
||
| public static async Task DemonstrateDeadlockRiskAsync() | ||
| { | ||
| var tasks = Enumerable.Range(0, 25) | ||
| .Select(_ => Task.Run(() => Foo(() => SomeIOOperationAsync()))); | ||
| await Task.WhenAll(tasks); | ||
| } | ||
|
|
||
| private static async Task<int> SomeIOOperationAsync() | ||
| { | ||
| await Task.Delay(100); | ||
| return 42; | ||
| } | ||
| } | ||
| // </ThreadPoolDeadlock> | ||
|
|
||
| // <ConfigureAwaitMitigation> | ||
| public static class ConfigureAwaitMitigation | ||
| { | ||
| public static async Task<int> LibraryMethodAsync() | ||
| { | ||
| await Task.Delay(100).ConfigureAwait(false); | ||
| return 42; | ||
| } | ||
|
|
||
| public static int Sync() | ||
| { | ||
| return LibraryMethodAsync().GetAwaiter().GetResult(); | ||
| } | ||
| } | ||
| // </ConfigureAwaitMitigation> | ||
|
|
||
| // Verification entry point | ||
| public class Program | ||
| { | ||
| public static void Main() | ||
| { | ||
| Console.WriteLine("--- ConfigureAwait mitigation demo ---"); | ||
| int result = ConfigureAwaitMitigation.Sync(); | ||
| Console.WriteLine($"Result: {result}"); | ||
|
|
||
| Console.WriteLine("Done."); | ||
| } | ||
| } |
10 changes: 10 additions & 0 deletions
10
...tterns/snippets/synchronous-wrappers-for-async-methods/csharp/SyncWrappersForAsync.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> |
41 changes: 41 additions & 0 deletions
41
...ronous-programming-patterns/snippets/synchronous-wrappers-for-async-methods/vb/Program.vb
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,41 @@ | ||
| ' <SyncOverAsyncTAP> | ||
| Public Module TapWrapper | ||
| Public Function Foo(fooAsync As Func(Of Task(Of Integer))) As Integer | ||
| Return fooAsync().Result | ||
| End Function | ||
| End Module | ||
| ' </SyncOverAsyncTAP> | ||
|
|
||
| ' <DeadlockExample> | ||
| Public Module DeadlockExample | ||
| Private Sub Delay(milliseconds As Integer) | ||
| DelayAsync(milliseconds).Wait() | ||
| End Sub | ||
|
|
||
| Private Async Function DelayAsync(milliseconds As Integer) As Task | ||
| Await Task.Delay(milliseconds) | ||
| End Function | ||
| End Module | ||
| ' </DeadlockExample> | ||
|
|
||
| ' <ConfigureAwaitMitigation> | ||
| Public Module ConfigureAwaitMitigation | ||
| Public Async Function LibraryMethodAsync() As Task(Of Integer) | ||
| Await Task.Delay(100).ConfigureAwait(False) | ||
| Return 42 | ||
| End Function | ||
|
|
||
| Public Function Sync() As Integer | ||
| Return LibraryMethodAsync().Result | ||
| End Function | ||
| End Module | ||
| ' </ConfigureAwaitMitigation> | ||
|
|
||
| Module Program | ||
| Sub Main() | ||
| Console.WriteLine("--- ConfigureAwait mitigation demo ---") | ||
| Dim result As Integer = ConfigureAwaitMitigation.Sync() | ||
| Console.WriteLine($"Result: {result}") | ||
| Console.WriteLine("Done.") | ||
| End Sub | ||
| End Module |
9 changes: 9 additions & 0 deletions
9
...g-patterns/snippets/synchronous-wrappers-for-async-methods/vb/SyncWrappersForAsync.vbproj
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,9 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <RootNamespace>SyncWrappersForAsync</RootNamespace> | ||
| </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.