Overriding Stream Asynchrony

Stephen Toub - MSFT

In .NET 4.5 Beta, the Stream class provides multiple virtual methods related to reading and writing:

  • Read, BeginRead / EndRead, ReadAsync
  • Write, BeginWrite / EndWrite, WriteAsync
  • Flush, FlushAsync
  • CopyToAsync

As a developer deriving from Stream, it’s helpful to understand what the base implementations do and when you can and should override them.

Read, Write, Flush

The Read, Write, and Flush methods are the core synchronous mechanisms from reading and writing from and to a stream:

public abstract int Read(  byte[] buffer, int offset, int count);
public abstract void Write(byte[] buffer, int offset, int count);
public abstract void Flush();

You must override these methods. This isn’t just guidance: the methods are abstract, and thus all valid derivations must override them.  If your stream is only readable or only writable, or if you wanted to prohibit synchronous usage, you could choose to throw a NotSupportedException from the methods you don’t want consumers to be using, but you still must override the methods.

BeginRead/EndRead, BeginWrite/EndWrite

Since the early days of .NET, Stream has supported asynchronous reading and writing via the following methods that conform to the APM pattern:

public virtual IAsyncResult BeginRead(
    byte[] buffer, int offset, int count, AsyncCallback callback, object state);
public virtual int EndRead(IAsyncResult asyncResult);

public virtual IAsyncResult BeginWrite(
    byte[] buffer, int offset, int count, AsyncCallback callback, object state);
public virtual void EndWrite(IAsyncResult asyncResult);

If you don’t override BeginRead/EndRead or BeginWrite/EndWrite, their default implementation in the Stream class will simply queue up work to the ThreadPool to call the corresponding synchronous Read/Write method. This means that, by default, these Begin/End methods will be doing their work asynchronously with regards to the caller, but for the duration of the actual read or write operation, a thread from the ThreadPool will be blocked (see my previous blog post on offloading vs scalability). If you can provide a truly asynchronous implementation, one based on async I/O that won’t block threads while I/O operations are in progress, you should consider overriding these methods. 

ReadAsync, WriteAsync, FlushAsync

New to .NET 4.5, the Stream class provides three virtual methods for reading and writing asynchronously based on the Task-based Asynchronous Pattern:

public virtual Task<int> ReadAsync(
   
byte[] buffer, int offset, int count, CancellationToken cancellationToken);
public virtual Task WriteAsync(
    byte[] buffer, int offset, int count, CancellationToken cancellationToken);
public virtual Task FlushAsync(CancellationToken cancellationToken);

If you don’t override ReadAsync or WriteAsync, their default implementation in the Stream class will simply use the BeginRead/EndRead or BeginWrite/EndWrite methods to perform the work, returning a Task<int> or Task to represent those calls, in a manner extremely similar to how Task.Factory.FromAsync creates Tasks from Begin/End pairs. So, if you only override the synchronous methods and none of the asynchronous ones, ReadAsync/WriteAsync will return a Task that represents Read/Write running on the ThreadPool, via the default implementation of BeginRead/EndRead/BeginWrite/EndWrite. If you have overridden BeginRead/EndRead/BeginWrite/EndWrite, then ReadAsync/WriteAsync will pick up that custom implementation and use it for its asynchrony. You can of course override ReadAsync/WriteAsync as well, regardless of whether you’ve overridden the Begin/End methods; this is in particular a good idea if you are able to support cancellation throughout the processing of the asynchronous operation, since the XxAsync methods accept a CancellationToken.

In some cases, you may have found it too difficult or error prone to implement the Begin/End methods, even if an asynchronous implementation would have been possible.  In such cases, you might consider overriding ReadAsync/WriteAsync without overriding the Begin/End methods. This will typically be made significantly easier via the C# and Visual Basic support for writing async methods using await.  Then, once you have your Task-based implementations, you might even consider overriding the Begin/End methods and implementing them in terms of the Task-based methods.

The FlushAsync method is the asynchronous counterpart to the synchronous Flush method. The base implementation of Stream.FlushAsync queues to the ThreadPool a call to Flush. If your Stream has a nop Flush method, it’s a good idea to override FlushAsync to simply return an already completed task, as there’s no need to spend energy queueing a work item that itself will be a nop.  Of course, if you can implement FlushAsync using async I/O rather than burning a ThreadPool thread while invoking the synchronous variant, that’s a good thing to consider doing.

CopyToAsync

.NET 4.5 also includes a virtual, asynchronous counterpart to the synchronous (non-virtual) CopyTo method that was introduced in .NET 4:

public virtual Task CopyToAsync(
    Stream destination, int bufferSize, CancellationToken cancellationToken);

The base Stream.CopyToAsync method uses the ReadAsync method on the source stream and the WriteAsync method on the destination stream, calling them in a loop to transfer data from one stream to the other.  However a derived type could choose to override CopyToAsync to provide a more specialized implementation.  For example, MemoryStream already has all of its data buffered in memory, so it can achieve the copy with a single WriteAsync of the entire buffer to the target stream, or even a straight buffer copy if the target is also a MemoryStream.

0 comments

Discussion is closed.

Feedback usabilla icon