Async or non-blocking?

Spread the love

It is often helpful to distinguish between “truly async” code and merely “non-blocking” code. A program that dispatches long running operations to the thread pool would generally be non-blocking since it doesn’t tie up the calling thread. In contrast, a program that reads from a network socket using overlapped I/O would be truly async.

With that in mind, is this code truly async or just non-blocking?

Task WriteSomeBytesAsync(FileStream stream)
{
    byte[] buffer = Encoding.ASCII.GetString("Some data for you");
    return stream.WriteAsync(buffer, 0, buffer.Length);
}

As it turns out, we can’t know for sure without additional context.

If the FileStream in question was opened with useAsync = false (the default), then it is definitely just non-blocking; all underlying writes would be synchronous but offloaded to the thread pool.

If it was instead opened in async mode, then we have two situations to consider. Given that file stream I/O is buffered, the write may end up filling an internal buffer; in that case, the call would be non-blocking. However, if the write would overflow the buffer, the call would be truly async since it would need to make a corresponding overlapped call to the underlying native file handle.

One quick and dirty (read: “not generally reliable”) way of detecting if a task was truly async is shown in the code sample below. It uses the heuristic of checking the current stack for an “IOCompletion” method frame.

private static async Task<bool> WasIOThreadAsync(Task incoming)
{
    await incoming;

    foreach (StackFrame frame in new StackTrace(true).GetFrames())
    {
        MethodBase method = frame.GetMethod();
        if (method.Name.IndexOf("IOCompletion", 0, StringComparison.OrdinalIgnoreCase) >= 0)
        {
            return true;
        }
    }

    return false;
}

And here is a sample use case illustrating the information above:

private static async Task WriteToFileAsync(bool useAsync)
{
    using (FileStream file = new FileStream(@"G:\Temp\file.txt", FileMode.Create, FileAccess.Write, FileShare.None, 128, useAsync))
    {
        byte[] buffer = Encoding.ASCII.GetBytes("0-----------------------------" + Environment.NewLine);
        for (int i = 0; i < 64; ++i)
        {
            buffer[0] = (byte)('1' + i);
            bool wasIOThread = await WasIOThreadAsync(file.WriteAsync(buffer, 0, buffer.Length));
            if ((i % 16) == 0)
            {
                Console.WriteLine();
            }

            Console.Write(wasIOThread ? 'I' : '.');
        }
    }
}

This code writes 32 bytes at a time to a file stream with an underlying buffer of size 128 and tracks how many calls in a row were serviced by [non-]I/O threads. When run with useAsync = true I got these results:
....I....I....I.
...I....I....I..
..I....I....I...
.I....I....I....

As expected, I see four non-I/O thread calls (4 x 32 = 128) to fill the buffer followed by one I/O thread result which flushes and writes.

Certainly it is best to be truly async when you can. But in practice, when you are at the mercy of framework class implementations, you may just have to settle for non-blocking.

Leave a Reply

Your email address will not be published. Required fields are marked *