Vinicius Quinafelex Alves

🌐Ler em português

[C#] Async programming, I/O and scalability

Asynchronous programming is a technique applied on a process so it's main threads do not execute long-running commands. Instead, those commands are assigned to external handlers to be executed in parallel, so the main threads become free to execute other commands.

It's often implemented with interrupts, so a thread is freed when an asynchronous command starts, and a thread is requested when the asynchronous command ends. That way, the process can ensure the execution continues with the same context, such as variables values and resources.

It's a foundation for platforms like NodeJS, that runs only a single main thread.

Synchronous program

Without asynchrony, a single thread could only handle multiple calls in sequence, as demonstrated below. Each call takes 10 seconds, so 3 calls require 30 seconds to be completed.

public Result Execute(string input)
{
    // Command A - CPU-only - 2 seconds
    var value = TransformInput(input);

    // Command B - I/O - 6 seconds
    var saveResult = SaveToDatabase(value);

    // Command C - CPU-only - 2 second
    return TransformOutput(saveResult);
}
Thread
Call 1
A
B
C
Call 2
A
B
C
Call 3
A
B
C

I/O commands

The main functionality of the command "SaveToDatabase" is to communicate with an external resource, making it an I/O command. During I/O commands, the thread does nothing while waiting for a result, but is still flagged as busy.

Some of the most common I/O commands are: connecting and sending commands to a database, reading/writing files on a hard disk, and communicating with external systems using through the network, such as third-party APIs.

Asynchronous program

On C#, some commands, including I/O commands, can be rewriten to be run asynchronously using the async/await library.

When a thread finds an await keyword, the command will be forwarded to a handler, and the thread is freed to execute something else. The process will be notified when the command is completed, so it can assign a free thread to continue executing the method.

public async Task<Result> ExecuteAsync(string input)
{
    // Operation A - CPU-only - 2 seconds
    var value = TransformInput(input);

    // Operation B - I/O - 6 seconds
    var saveResult = await SaveToDatabaseAsync(value);

    // Operation C - CPU-only - 2 second
    return TransformOutput(saveResult);
}
Thread
Call 1
A
C
Call 2
A
C
Call 3
A
C
Handler
B
B
B

As the I/O command is forwarded to a handler, the thread is free to immediatelly start executing the next call. So the 3 calls that previously took 30 seconds now are executed in 14 seconds, without requiring additional threads or upgrading the hardware.