asynchronous programming
lambda expressions
C# programming
Write method
programming techniques

Using lambda expression instead of asynchronous version of Write method

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When performing I/O operations in C#, developers sometimes wrap a synchronous Write call inside a lambda and pass it to Task.Run, believing this achieves the same result as calling the built-in WriteAsync method. While both approaches free the calling thread, they work in fundamentally different ways under the hood, and choosing the wrong one can waste thread pool resources and hurt scalability. Understanding this distinction is critical for writing efficient asynchronous code in .NET.

Why Async I/O APIs Exist

Operating systems provide asynchronous I/O primitives that allow a write request to be submitted and completed later without blocking any thread. The .NET Stream.WriteAsync method leverages these OS-level primitives. When you call WriteAsync, the runtime submits the I/O request and returns a Task immediately. No thread sits idle waiting for the disk or network to finish. The thread pool thread is free to handle other work.

This is why the framework provides dedicated async methods rather than expecting developers to wrap synchronous calls manually. True async I/O scales far better because it does not consume a thread during the wait.

The Lambda Wrapper Approach

A common pattern developers reach for is wrapping the synchronous Write method in a Task.Run lambda:

csharp
1// Wrapping synchronous Write in Task.Run — NOT recommended for I/O
2await Task.Run(() =>
3{
4    using var stream = new FileStream("output.dat", FileMode.Create);
5    stream.Write(buffer, 0, buffer.Length);
6});

This does move the blocking call off the calling thread, which can be useful in a UI application to keep the interface responsive. However, it still blocks a thread pool thread for the entire duration of the I/O operation. Under heavy load, this approach exhausts the thread pool and creates a bottleneck.

The True Async Approach

The correct approach for I/O-bound work is to use the async API directly:

csharp
1// True async I/O — recommended
2await using var stream = new FileStream(
3    "output.dat",
4    FileMode.Create,
5    FileAccess.Write,
6    FileShare.None,
7    bufferSize: 4096,
8    useAsync: true);
9
10await stream.WriteAsync(buffer, 0, buffer.Length);

Notice the useAsync: true parameter in the FileStream constructor. This tells .NET to use overlapped I/O on Windows, enabling genuine asynchronous operations. Without this flag, even WriteAsync may fall back to blocking a thread pool thread internally.

Side-by-Side Comparison

Here is a practical example showing both approaches in a method that writes data to a file:

csharp
1public class FileWriterComparison
2{
3    private readonly byte[] _data;
4
5    public FileWriterComparison(byte[] data) => _data = data;
6
7    // Approach 1: Lambda wrapping synchronous Write
8    public Task WriteSyncWrappedAsync(string path)
9    {
10        return Task.Run(() =>
11        {
12            using var fs = new FileStream(path, FileMode.Create);
13            fs.Write(_data, 0, _data.Length);
14        });
15    }
16
17    // Approach 2: True async WriteAsync
18    public async Task WriteTrueAsync(string path)
19    {
20        await using var fs = new FileStream(
21            path, FileMode.Create, FileAccess.Write,
22            FileShare.None, 4096, useAsync: true);
23        await fs.WriteAsync(_data, 0, _data.Length);
24    }
25}

Both methods return a Task and can be awaited. The difference is invisible to the caller but significant at scale. WriteSyncWrappedAsync ties up a thread pool thread for the entire write, while WriteTrueAsync releases it during the actual I/O wait.

When Each Approach Is Appropriate

The lambda-with-Task.Run pattern is not always wrong. It is appropriate in specific situations:

Use Task.Run with a lambda when:

  • You are in a UI application (WPF, WinForms) and need to move CPU-bound or legacy synchronous work off the UI thread.
  • You are calling a third-party library that provides no async API and you cannot change it.

Use the native async API when:

  • You are writing server-side code (ASP.NET, background services) where thread pool efficiency matters.
  • The framework provides an async version of the method (which is the case for all modern .NET I/O APIs).
  • You need to handle high concurrency without exhausting threads.
csharp
1// In ASP.NET — always prefer the async API
2[HttpPost("upload")]
3public async Task<IActionResult> Upload(IFormFile file)
4{
5    await using var stream = new FileStream(
6        "uploads/" + file.FileName,
7        FileMode.Create, FileAccess.Write,
8        FileShare.None, 4096, useAsync: true);
9
10    await file.CopyToAsync(stream);
11    return Ok();
12}

Common Pitfalls

  • Using Task.Run for I/O in ASP.NET: This wastes a thread pool thread and adds overhead from the thread switch without any benefit, since there is no UI thread to unblock.
  • Forgetting useAsync: true on FileStream: Without this flag, WriteAsync internally blocks a thread pool thread, negating the benefit of the async call.
  • Mixing sync and async in the same stream: Calling both Write and WriteAsync on the same stream can cause race conditions and corrupted output.
  • Not awaiting the returned task: Forgetting await on WriteAsync means the write may not complete before the stream is disposed.
  • Wrapping WriteAsync inside Task.Run: This double-wraps the operation, wasting a thread pool thread to await an already-async operation.

Summary

  • Stream.WriteAsync uses OS-level async I/O and releases the thread during the wait, making it the right choice for server-side and high-concurrency scenarios.
  • Wrapping synchronous Write in Task.Run(() => ...) still blocks a thread pool thread and should only be used to offload work from a UI thread or when no async API exists.
  • Always pass useAsync: true when constructing a FileStream to ensure WriteAsync uses true asynchronous I/O.
  • On ASP.NET and other server-side frameworks, prefer native async APIs over Task.Run wrappers to maintain thread pool health under load.

Course illustration
Course illustration

All Rights Reserved.