An not so asynchronous mistake anyone can make when using the System.IO.Stream class.
Got a stack overflow exception come in today for an application I'm working on. Turns out if you send rather large files around then all of a sudden this guy just pops up and takes you down. I have to say this wasn't something I was expecting, and was actually pretty sure the code we were running was stable.
What caused the problem? Turns out Stream.BeginRead isn't so asynchronous after all. In fact Stream.BeginRead just does a Read operation and then immediately calls your callback. Okay, so it is up to derived classes to support BeginRead if they want asynchronous operation. Well, I have to say my friend, that sucks. After all, you spend a good deal of time building a model and you expect that model to work. Take the following asynchronous aware code to read a stream all the way to the end.
myState.Stream.BeginRead(myState.Buffer, 0, myState.Buffer.Length, new AsyncCallback(ReadMore), myState);
private void ReadMore(IAsyncResult ar) {
MyState myState = (MyState) ar.AsyncState;
int bytesRead = myState.Stream.EndRead(ar);
if ( bytesRead > 0 ) {
// Do some work
myState.Stream.BeginRead(myState.Buffer, 0, myState.Buffer.Length, new AsyncCallback(ReadMore), myState);
} else {
// Our operation is done. Do some final work
}
}
Can you spot the the StackOverflow yet? Well, since we are calling ReadMore as our asynchronous callback delegate, and since things are actually happening synchronously behind the scenes, each call to ReadMore is recursing further into our stack space. You get 1 stack frame for ReadMore and one stack frame for BeginRead each time through. This is what the two patterns look like if BeginRead is really asynchronous and if it cheats.
Thread1 stack:
CurrentMethod() calls BeginRead() which returns
Thread2 stack:
ReadOperation() calls AsyncCallback() calls EndRead() calls BeginRead() which returns
Thread1 stack:
CurrentMethod() calls BeginRead() calls ReadOperation() calls AsyncCallback() calls EndRead() calls BeginRead() calls repeat...
You see the difference? Big difference. You probably wouldn't even hit this in your program unless you made a LOT of reads. Takes a lot of nested reads to exhaust the stack. Now a bunch of people are probably raising their hands saying “I knew that! I knew they cheated and I wrote my code accordingly.” How do you write your code accordingly? If you want to support generic reading from files, the network, and memory, you have to take a Stream class. Now how in the hell do you write code to asynchronously read from all of those stream types and truly alleviate the stack overflow? I have some ideas, namely abstracting out a class that knows how to consume streams asynchronously without using BeginRead/EndRead. In the case of various types of streams you could even special case it to use the real asynchronous behavior instead of your own. If I come up with anything solid, maybe I'll share it.