Back to Home

When to Use async/await vs Task.Run in C#

async/await is for I/O-bound operations (file access, network calls, database queries) that naturally support asynchronous operations. Task.Run is for CPU-bound work that you want to offload to a background thread.

Use async/await for inherently asynchronous APIs (HttpClient, file I/O, database operations). Use Task.Run to run synchronous CPU-intensive work on a thread pool thread without blocking the UI or request thread.

Mixing them incorrectly can hurt performance. Don't use Task.Run for I/O operations that already have async APIs, and don't use async/await for purely synchronous CPU work.

Setup and Main Method

C# Example Code
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public class AsyncAwaitVsTaskRun
{
    public static async Task Main(string[] args)
    {
        Console.WriteLine($"Main thread: {Thread.CurrentThread.ManagedThreadId}");

        // async/await - for I/O-bound operations
        Console.WriteLine("\n=== async/await for I/O operations ===");
        await IoOperationExample();

        // Task.Run - for CPU-bound operations
        Console.WriteLine("\n=== Task.Run for CPU-bound work ===");
        await CpuBoundExample();

        // Common mistake: wrapping async in Task.Run
        Console.WriteLine("\n=== Anti-pattern: Don't do this ===");
        await AntiPattern();

        // Proper pattern for UI applications
        Console.WriteLine("\n=== Proper pattern for UI apps ===");
        await UiPatternExample();
    }

async/await for I/O-Bound Operations

Use async/await for I/O operations like file reading. The thread is released while waiting for the operation to complete, allowing other work to proceed.

C# Example Code
// async/await: I/O-bound operation (file reading)
static async Task IoOperationExample()
{
    Console.WriteLine($"Before I/O: Thread {Thread.CurrentThread.ManagedThreadId}");

    // This uses async I/O - thread is released while waiting
    string content = await File.ReadAllTextAsync("example.txt")
        .ContinueWith(t => t.IsFaulted ? "File not found" : t.Result);

    Console.WriteLine($"After I/O: Thread {Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"Content length: {content.Length}");
}

Task.Run for CPU-Bound Operations

Use Task.Run to offload CPU-intensive work to a background thread pool thread, preventing it from blocking the main thread.

C# Example Code
// Task.Run: CPU-bound operation (heavy computation)
static async Task CpuBoundExample()
{
    Console.WriteLine($"Before CPU work: Thread {Thread.CurrentThread.ManagedThreadId}");

    // Offload CPU-intensive work to thread pool
    int result = await Task.Run(() =>
    {
        Console.WriteLine($"CPU work on: Thread {Thread.CurrentThread.ManagedThreadId}");
        return PerformHeavyCalculation(1000000);
    });

    Console.WriteLine($"After CPU work: Thread {Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"Result: {result}");
}

Anti-Pattern: Don't Wrap Async in Task.Run

Wrapping an already async operation in Task.Run adds unnecessary overhead. Always await async APIs directly.

C# Example Code
// Anti-pattern: Don't wrap async operations in Task.Run
static async Task AntiPattern()
{
    // BAD: Unnecessary Task.Run for already async operation
    var badResult = await Task.Run(async () =>
    {
        using HttpClient client = new HttpClient();
        return await client.GetStringAsync("https://api.example.com");
    });

    // GOOD: Just await the async operation directly
    using HttpClient client = new HttpClient();
    try
    {
        var goodResult = await client.GetStringAsync("https://api.example.com");
        Console.WriteLine("Direct await is better for I/O");
    }
    catch (HttpRequestException)
    {
        Console.WriteLine("Network error (expected in example)");
    }
}

Keeping UI Responsive with Task.Run

In UI applications, use Task.Run to run CPU-intensive work on a background thread while keeping the UI thread free to update the interface.

C# Example Code
// UI pattern: Keep UI responsive
static async Task UiPatternExample()
{
    // In a UI app, use Task.Run for CPU work to keep UI responsive
    var progress = 0;

    var cpuTask = Task.Run(() =>
    {
        for (int i = 0; i < 10; i++)
        {
            Thread.Sleep(100);  // Simulate work
            progress = (i + 1) * 10;
        }
        return progress;
    });

    // UI thread can update display while work happens
    while (!cpuTask.IsCompleted)
    {
        Console.WriteLine($"Progress: {progress}%");
        await Task.Delay(50);
    }

    Console.WriteLine($"Completed: {await cpuTask}%");
}

Helper Method

C# Example Code
// Helper: Simulate CPU-intensive work
static int PerformHeavyCalculation(int iterations)
{
    int result = 0;
    for (int i = 0; i < iterations; i++)
    {
        result += i % 7;
    }
    return result;
}

Take It Further

Concurrency in C# Cookbook book cover

Struggling with async/await? The definitive recipe book for every concurrency pattern in .NET.

Concurrency in C# Cookbook — 2nd Edition · Stephen Cleary

Get it on Amazon →

As an Amazon Associate I earn from qualifying purchases.