Skip to content
Merged
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
4 changes: 4 additions & 0 deletions docs/navigate/advanced-programming/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ items:
href: ../../standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern.md
- name: Interop with other asynchronous patterns and types
href: ../../standard/asynchronous-programming-patterns/interop-with-other-asynchronous-patterns-and-types.md
- name: Asynchronous wrappers for synchronous methods
href: ../../standard/asynchronous-programming-patterns/async-wrappers-for-synchronous-methods.md
- name: Synchronous wrappers for asynchronous methods
href: ../../standard/asynchronous-programming-patterns/synchronous-wrappers-for-asynchronous-methods.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,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 to the rule

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)
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>
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.");
}
}
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>
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
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.");
}
}
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>
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
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>
Loading
Loading