I/O-bound or CPU-bound?

1 min read

The kind of work decides how you use async/awaitAsync and awaitAwait frees the thread while an operation waits, and the method picks up where it left off.Opens in a new tab.

I/O-bound

I/O-bound operations involve waiting for external resources such as network responses, database queries or file I/O. async/await is highly effective here because it releases the thread during the waiting, which makes it available for other tasks. That improves application responsiveness and scalability.

The await is where the thread is released while the network call runs.

C#
// Example for I/O-bound operation
public async Task<string> DownloadWebContentAsync(string url)
{
    using (var client = new HttpClient())
    {
        // The thread is released here while waiting for the network response.
        string content = await client.GetStringAsync(url);
        return content;
    }
}

CPU-bound

CPU-bound operations involve intensive computation that uses a lot of CPU time, like complex calculations or data processing. To avoid blocking the UI thread, or a request-handling thread in a server application, offload the CPU-bound work to a background thread with Task.Run. Then await the result.

The work inside Task.Run is a lambdaLambda expressionsA short way to write a method inline: an anonymous function.Opens in a new tab, passed to it as a Func<long> delegateDelegatesA type-safe function pointer, so a method can be passed around like any other value.Opens in a new tab. The Thread.Sleep only simulates work, and its own comment says to avoid it in real async CPU-bound work.

C#
// Example for CPU-bound operation
public async Task<long> CalculateFactorialAsync(int number)
{
    // Offload the CPU-intensive calculation to a thread pool thread.
    long result = await Task.Run(() =>
    {
        long fact = 1;
        for (int i = 1; i <= number; i++)
        {
            fact *= i;
            // Simulate work
            Thread.Sleep(10); // Avoid Thread.Sleep in real async CPU-bound work; this is for demo
        }
        return fact;
    });
    return result;
}

A real job is both

Most real jobs mix the two, so each phase is classified separately. A web endpoint that accepts a photo and stores a thumbnail has four phases: reading the upload (I/O), resizing the image (CPU-bound), writing the thumbnail to storage (I/O), and updating the database record (I/O). Only the resize keeps a core busy. The other three are waits, so they use await.

C#
public async Task<IActionResult> Upload(IFormFile photo)
{
    using var input = photo.OpenReadStream();
    var image = await Image.LoadAsync(input);                    // I/O: read the upload
    image.Mutate(x => x.Resize(200, 200));                       // CPU-bound: resize
    await storage.SaveThumbnailAsync(photo.FileName, image);     // I/O: write to storage
    await db.SaveChangesAsync();                                 // I/O: update the record
    return Ok();
}

The resize runs directly, because a request handler is already on a pool thread (see Task.RunTask.Run: when it helps and when it doesn'tTask.Run moves CPU work to a pool thread. It pays off when the current thread must stay free.Opens in a new tab). How to classify a call in a library is in CPU-bound or I/O-bound library methodsIs this library method CPU-bound or I/O-bound?The Async suffix does not say. Six checks, in order of effort.Opens in a new tab.


Sources