October 24th, 2011

When at last you await

Stephen Toub - MSFT
Partner Software Engineer

When you start using async methods heavily, you’ll likely see a particular pattern of composition pop up from time to time.  Its structure is typically either of the form:

async Task FooAsync()
{
    … // some initialization code without awaits 
    await BarAsync(…);
}

or of the form:

async Task<T> FooAsync()
{
    … // some initialization code without awaits
    return await BarAsync(…);
}

A concrete example of this might be a FlushAsync method on a custom buffered stream, where the stream maintains a byte[] of data to write out, along with the number of bytes buffered, and flushing the buffer involves resetting the buffered count and writing the data out to the underlying wrapped stream, e.g.

public override async Task FlushAsync()
{
    int numBuffered = m_numBuffered;
    m_numBuffered = 0;
    await m_wrappedStream.WriteAsync(m_buffer, 0, numBuffered);
}

When we find code that follows this particular pattern, we can employ an alternative.  Instead of the above, we could write this FlushAsync method as follows:

public override Task FlushAsync()
{
    int numBuffered = m_numBuffered;
    m_numBuffered = 0;
    return m_wrappedStream.WriteAsync(m_buffer, 0, numBuffered);
}

The key difference here is that this method is no longer using the compiler support for writing async methods.  By removing the “async” keyword, this is just an ordinary method, rather than one that the compiler rewrites into a state machine.  And along with that, we’re now returning a Task created by the body of the method rather than relying on the compiler to create one for us.

Why would we want to do this?  Purely as an optimization.  If the particular code in question is very performance-sensitive, you can save some cycles by avoiding the overhead necessary to support compiler-generated async methods.  When you use the async/await keywords, the compiler and Framework collude to generate a Task on your behalf to represent all of the async method’s processing.  In this case, however, we’re being handed a task to represent the last statement in the method, and thus it’s in effect already a representation of the entire method’s processing (as long as nothing in the preamble could throw an exception, since such an exception would directly escape the method rather than being stored into the returned task).  Since it already provides the representation we need, we can return it untouched to our caller, saving a level of indirection.

For more information on performance and async methods, see the October 2011 issue of MSDN Magazine.

Author

Stephen Toub - MSFT
Partner Software Engineer

Stephen Toub is a developer on the .NET team at Microsoft.

0 comments

Discussion are closed.