Thursday, November 20, 2008

BeginRead Weirdness

I've run into a bit of an interesting problem with some code I'm working on.  It deals with asynchronous reads on a stream.  Here is a simplified version of the problem:

I have a network device that communicates by a TCP socket.  The client software connects to that TCP socket and issues commands.  Every command should receive a response from the device.  A new command must not be sent until the response to the last command was received.

So initially I wrote some code that looked like this:

public class Reader
{
private readonly Stream inStream;

public Reader( Stream in )
{
inStream = in;
}

public byte[] ReadResponse()
{
byte[] buffer = new byte[256]; 
inStream.Read(buffer, 0, buffer.Length);
return buffer;
}
}

This code worked just fine...as long as the device responded to my command.  Sometimes, the device wouldn't respond.  This might be because the device was unplugged before the response was sent (in which case my Read attempt would eventually return indicating the end of the stream was reached) or the device simply failed to respond to the command.

So now what I wanted was a way to deal with the device simply not responding to a command.  I needed some mechanism that would allow me to block on the read for only a little while.  A perfect place to use the asynchronous read!  So I modified my code to this:

public byte[] ReadResponse()
{
byte[] buffer = new byte[256]; 
IAsyncResult waitHandle 
= inStream.BeginRead(
buffer, 
0, 
buffer.Length, 
null, 
null);
bool waitResult 
= waitHandle.AsyncWaitHandle.WaitOne(
5000, 
true);
return buffer;
}

This change will read back the response if one is returned, or stop blocking after 5 seconds elapses without a response.  This works as I expected - the WaitOne returns as soon as data is available, or after the timeout period.  If the waitResult indicates that the timeout occurred, I can send my next command.  Here is where the weirdness is: the BeginRead attempting to read the response to the first command now reads the response to the second command!  This results in my latest command never receiving a response. 

So my question is, how can I block on a read for a specified period of time and avoid consuming the next byte arriving on the stream if the timeout occurs?  

I've tried a couple of things, and I have a working solution, but I'm not satisfied with it.  My current solution is to use a NetworkStream rather than a base Stream.  I would prefer to use the basic Stream type as it allows me to apply this reader to any Stream source and not just NetworkStream sources.  The NetworkStream has a DataAvailable property that returns true when data is available to be read on the stream.  I poll the DataAvailable property until data arrives or my 5 second timeout occurs.  This avoids the erroneously consumed byte.  However, in addition to the reliance on a NetworkStream source, it also introduces inefficiency as I poll the DataAvailable property.  I can either cause the CPU to spike by continuously polling the property, or I can introduce a Sleep.  The Sleep will allow the CPU usage to remain low, but also means that if the data becomes available while the thread is sleeping, I'm wasting time that could be spent processing the data.

What I really need is a way to cancel the thread that is waiting for the response asynchronously.  Unfortunately, the .NET framework doesn't support this concept.  So my only option to get this working the way I really want is to craft up my own Asynchronous read method.  Unfortunately, I can't think of a way to do this that avoids the polling scenario I have already implemented.

2 comments:

  1. Here's a general response from someone who doesn't speak .net.

    Is it possible to have your reads always place the response they receive in some sort of labeled data structure container?

    Whenever a read completes, the reader could spawn a process to that searches the container for the correctly labeled piece of data. That way you would always pair your requests up with the expected responses, even if you somehow switch to a non-blocking approach.

    ReplyDelete
  2. That's a good idea. The tricky part will be keeping the stream used for reading responses in sync with the stream used for writing commands. Let's say I don't get a response, and it is due to the TCP connection to this device being dropped. I now need to create a new TcpClient connection and replace the stream used both in the global reader and in the writer.

    ReplyDelete