Asynchronous I/O in C#: Introduction

Last week with Mariano Converti, we gave the first in a series of presentations related to how asynchronous I/O works in C#. The idea is to start from the low level components that allow asynchronous I/O to take place and to finish with how the async/await constructs work. As each of the presentations takes place, either Mariano or I will write a blog post summarizing the core concepts and also providing a link to a GitHub repo with all the samples.

In this introductory post, I would like to go over some of the benefits of using asynchronous I/O together with some of the drawbacks.

Why should you care?

Simply put:

Asynchronous I/O does not block the thread while performing operations such as disk or network access.

This is really important, as there is a huge difference (orders of magnitude) in the access time between accessing main memory and retrieving something from the network as this Stack Overflow answer explains.

Whenever you perform a synchronous I/O operation the executing thread is idle waiting for the operation to complete. The worse thing is that when waiting for the operation to complete it is doing nothing, but it cannot perform any other work! So in the meantime, if it is a UI thread your UI will be blocked, and if it is a thread from the IIS Thread Pool it won’t be able to process any other requests.

An enlightening example

Let’s take the following figure as an example (I’m using IIS for the example as the UI blocking one is something all of you have probably seen). The drawing on the left represents an IIS Thread Pool thread that only performs synchronous I/O. The one on the right represents a similar thread, which uses asynchronous I/O.

Let’s assume that 100 requests come in simultaneously, that we only have one thread to serve all of them and that each request that comes in follows this sequence:

  1. Get request and process: 5 ms.
  2. Read from DB: 490 ms.
  3. Process and provide response: 5 ms.

Important: The example is really simple, but it does help illustrate a point.

image

Note: If the math is incorrect, please leave a comment and I will fix the error. It’s over 1 AM so I could have made a mistake.

It is easy to see from the figure that the thread that uses synchronous I/O can only reply to two of those requests in one second.

On the other hand, the one that takes advantage of asynchronous I/O could reply to all 100 of them.

Yes, that is 50 times more, which begs the question…

Why haven’t you been using this everywhere?

Before the async/await constructs were available in C# (which are commonly used together with the Task-based asynchronous pattern or TAP), performing asynchronous I/O required the usage of patterns such as the asynchronous programming model (APM) the event-based asynchronous pattern (EAP) and the plain TAP (plain = without async/await). All of them require coders to split the I/O access code between the instructions prior to the operations and those that deal with the result (either through the use of callbacks, events or continuations). For example, to read contents from a file using the APM you would need code like this:

You can’t deny that the above seems like a bit too much to read the contents from a file. Doesn’t the following blocking code look much cleaner?

Well, it does… but is also blocks. Which leads to the following conclusion:

Not all I/O operations need to be asynchronous. Before going for asynchrony, consider what it implies.

Coming Soon…

The async/await constructs do allow you to write code that looks like the second sample, while getting to keep the benefits of the first one.

The fact that this constructs are so magical powerful means that developers don’t need completely understand how things are working under the hood. Nevertheless, in many scenarios it is important to understand how this operations work in order to take full advantage of them and use them correctly.

The next blog post, based on the first talk, will deal with a kind of low level component that is heavily used in Windows to support asynchronous I/O: I/O Completion Ports.