locked
How would I go about making an Async implementation for Extension methods to the SerialPort class?

    Question

  • I'm talking about things like:

    void WriteLineAsync(string);

    Task<string> ReadLineAsync();

    Task<string> ReadToAsync(string);

     

    Can anyone take a crack at one of these so that I can get a better understanding of wrapping old code up asynchronously?  I get parallel processing, but working with IO has my head spinning on how to do it without spawning threads, or am I going to have to spawn them?  Can't I register for hardware events somehow?

    Wednesday, November 24, 2010 12:32 AM

Answers

  • SerialPort is a bit odd in its design. There's a lower-level API and a higher-level API mixed up together.

    The lower-level API includes the methods for reading and writing bytes and byte arrays, and also anything dealing with BaseStream directly. The higher-level API uses Encoding to treat the data as strings or characters; this includes methods like ReadLine and ReadTo. Only the lower-level API supports asynchronous operations (via the BaseStream property); the higher-level API is only synchronous.

    The Async CTP already has ReadAsync and WriteAsync extension methods defined for Stream objects, so it's already possible to read/write byte arrays using *Async methods.

    ReadLine, ReadTo, and friends are more problematic. What you have is an API that only supports blocking methods. You must choose between three paths (assuming you don't want to use Rx):

    1. Re-implement the API to support asynchronous operations. e.g., write your own SerialPort class with ReadLineAsync and ReadToAsync, completely ignoring the existing ReadLine and ReadTo implementations.
    2. Write a "fake asynchronous" API. e.g., write ReadLineAsync and ReadToAsync which do use the existing ReadLine and ReadTo implementations. The problem with this approach is that it keeps a thread busy doing nothing but blocking.
    3. Extend SerialPort to support TextWriter and TextReader (which already supports ReadLineAsync), and re-implement any missing parts of the API such as ReadToAsync.

    If you need the best performance, or if you are writing reusable library code, I recommend (3). If you want the easier-to-code path, then I recommend (2). I focus on writing libraries, so I lean towards (3), but (2) has been done even in the BCL (in the Stream class, coincidentally). (1) is more of an "exercise".

    Here's an example implementing ReadToAsync using approach (1); this code is 100% untested:

    using System.IO.Ports;
    using System.Text;
    
    public sealed class MySerialPort
    {
      private readonly SerialPort serialPort;
      private readonly Decoder decoder;
      private readonly byte[] byteBuffer;
    
      public MySerialPort(SerialPort serialPort, Encoding encoding)
      {
        // Proper constructors omitted.
        this.serialPort = serialPort;
        this.decoder = encoding.GetDecoder();
        this.byteBuffer = new byte[1];
      }
    
      public async Task<string> ReadToAsync(string value)
      {
        // Parameter checking omitted.
    
        var ret = string.Empty;
    
        // Asynchronously read one byte at a time until our returned string contains the delimiter value.
        while (!ret.Contains(value))
        {
          int bytesRead = await this.serialPort.BaseStream.ReadAsync(byteBuffer, 0, 1); // Not the most efficient...
          var charBuffer = new char[this.decoder.GetCharCount(byteBuffer, 0, bytesRead, false)];
          this.decoder.GetChars(byteBuffer, 0, bytesRead, charBuffer, 0, false);
          ret += charBuffer;
        }
    
        return ret.Substring(0, ret.Length - value.Length);
      }
    }
    
    

    The code above does not include proper constructors, parameter checks, nor error handling (for both stream reads and Encoding errors). What's more, there's no guard against multiple asynchronous operations on the same serial port.

    The "fake asynchronous" code (2) is easier to write, but burns a thread:

    using System.IO.Ports;
    using System.Threading.Tasks;
    
    public static class SerialPortExtensions
    {
      public static Task<string> ReadToAsync(this SerialPort serialPort, string value)
      {
        // Parameter checking omitted.
    
        return Task.Factory.StartNew(() => serialPort.ReadTo(value));
      }
    }
    
    

    This code does omit parameter checking, but error conditions should be handled correctly. However, there's still no guard against multiple asynchronous operations on the same serial port.

    Personally, I would approach this particular problem from a very different perspective (3):

    1. Always treat a SerialPort as a (binary) Stream. Ignore the "higher-level" API.
    2. Define extension methods on SerialPort that allow creating a TextReader and TextWriter over the BaseStream.
    3. Note that TextReader already has a ReadLineAsync method on it.
    4. Define any other necessary extension methods (e.g., ReadToAsync) on TextReader/TextWriter, so they can be used by other text streams instead of just SerialPort.

    Even more untested code follows: 

    using System.IO;
    using System.IO.Ports;
    using System.Threading.Tasks;
    
    public static class MyExtensions
    {
      public static TextReader GetTextReader(this SerialPort serialPort)
      {
        // TODO: allow other StreamReader constructors.
        return new StreamReader(serialPort.BaseStream, serialPort.Encoding);
      }
    
      public static async Task<string> ReadToAsync(this TextReader textReader, string value)
      {
        // Parameter checking omitted.
    
        var ret = string.Empty;
        var buffer = new char[1]; // Not the most efficient...
        while (!ret.Contains(value))
        {
          var charsRead = await textReader.ReadAsync(buffer, 0, 1);
          if (charsRead == 0)
          {
            throw new EndOfStreamException();
          }
    
          ret += charsRead;
        }
    
        return ret.Substring(0, ret.Length - value.Length);
      }
    }
    
    

    This code omits parameter checking and full support for the StreamReader constructors, but it shows the approach that I would use. The downside is that you still have to re-implement ReadTo; the upside is that you would be implementing it only once for all TextReaders ever.

    But that's just the approach I would use. Your mileage may vary. :)

           -Steve

    P.S. If you're looking for an example of how to wrap old code asynchronously, I recommend wrapping code that already uses the Begin/End pattern. SerialPort is rather complex for a simple example.


    Programming blog: http://nitoprograms.blogspot.com/
      Including my TCP/IP .NET Sockets FAQ
      and How to Implement IDisposable and Finalizers: 3 Easy Rules
    Microsoft Certified Professional Developer

    How to get to Heaven according to the Bible
    • Marked as answer by Firoso Wednesday, November 24, 2010 7:39 PM
    Wednesday, November 24, 2010 7:25 PM