How to correctly use .NET2.0 serial port .BaseStream for async operation
Asked Answered
W

4

15

I am attempting to use the .BaseStream property of the .NET2.0 SerialPort to do asynchronous reads and writes (BeginWrite/EndWrite, BeginRead/EndRead).

I am having some success in this, but after a time, I notice (using Process Explorer) a very gradual increase in the Handles the app is using, and occasionally an extra thread, which also increases the Handle count.

The context switch rate also increases each time a new thread appears.

The app constantly sends 3 bytes to a PLC device, and gets 800 or so bytes in return, and does so at a baud rate of 57600.

The initial CSwitch Delta (again, from Process Explorer) is around 2500, which seems very high anyway. Each time a new thread appears, this value increases, and the CPU load increases accordingly.

I'm hoping that somebody might have done something similar, and can help me out, or even say 'In God's name, don't do it that way.'

In the code below, 'this._stream' is obtained from SerialPort.BaseStream, and CommsResponse is a class I use as the IAsyncresult state object.

This code is common to a TCP connection I make as an alternative to using the serial port, (I have a CommsChannel base class, with a serial and TCP channel derived from it) and it has none of these problems so I'm reasonably hopeful that there is nothing wrong with the CommsResponse class.

Any comments gratefully received.

    /// <summary>
    /// Write byte data to the channel.
    /// </summary>
    /// <param name="bytes">The byte array to write.</param>
    private void Write(byte[] bytes)
    {
        try
        {
            // Write the data to the port asynchronously.
            this._stream.BeginWrite(bytes, 0, bytes.Length, new AsyncCallback(this.WriteCallback), null);
        }
        catch (IOException ex)
        {
            // Do stuff.
        }
        catch (ObjectDisposedException ex)
        {
            // Do stuff.
        }
    }

    /// <summary>
    /// Asynchronous write callback operation.
    /// </summary>
    private void WriteCallback(IAsyncResult ar)
    {
        bool writeSuccess = false;

        try
        {
            this._stream.EndWrite(ar);
            writeSuccess = true;
        }
        catch (IOException ex)
        {
            // Do stuff.
        }

        // If the write operation completed sucessfully, start the read process.
        if (writeSuccess) { this.Read(); }
    }

    /// <summary>
    /// Read byte data from the channel.
    /// </summary>
    private void Read()
    {
        try
        {
            // Create new comms response state object.
            CommsResponse response = new CommsResponse();

            // Begin the asynchronous read process to get response.
            this._stream.BeginRead(this._readBuffer, 0, this._readBuffer.Length, new AsyncCallback(this.ReadCallback), response);
        }
        catch (IOException ex)
        {
            // Do stuff.
        }
        catch (ObjectDisposedException ex)
        {
            // Do stuff.
        }
    }

    /// <summary>
    /// Asynchronous read callback operation.
    /// </summary>
    private void ReadCallback(IAsyncResult ar)
    {
        // Retrieve the comms response object.
        CommsResponse response = (CommsResponse)ar.AsyncState;

        try
        {
            // Call EndRead to complete call made by BeginRead.
            // At this point, new data will be in this._readbuffer.
            int numBytesRead = this._stream.EndRead(ar);

            if (numBytesRead > 0)
            {
                // Create byte array to hold newly received bytes.
                byte[] rcvdBytes = new byte[numBytesRead];

                // Copy received bytes from read buffer to temp byte array
                Buffer.BlockCopy(this._readBuffer, 0, rcvdBytes, 0, numBytesRead);

                // Append received bytes to the response data byte list.
                response.AppendBytes(rcvdBytes);

                // Check received bytes for a correct response.
                CheckResult result = response.CheckBytes();

                switch (result)
                {
                    case CheckResult.Incomplete: // Correct response not yet received.
                        if (!this._cancelComm)
                        {
                            this._stream.BeginRead(this._readBuffer, 0, this._readBuffer.Length,
                                new AsyncCallback(this.ReadCallback), response);
                        }
                        break;

                    case CheckResult.Correct:  // Raise event if complete response received.
                        this.OnCommResponseEvent(response);
                        break;

                    case CheckResult.Invalid: // Incorrect response
                        // Do stuff.
                        break;

                    default: // Unknown response
                        // Do stuff.
                        break;
                }
            }
            else
            {
                // Do stuff.
            }
        }
        catch (IOException ex)
        {
            // Do stuff.
        }
        catch (ObjectDisposedException ex)
        {
            // Do stuff.
        }
    }
Whimper answered 20/1, 2009 at 19:22 Comment(0)
S
5

Some suggestions:

Since you are only sending 3 bytes, you could have a synchronous Write Operation. The delay wouldn't be much of an issue.

Also don't create a new AsyncCallback all the time. Create one Read and one Write AsyncCallback and use that in every begin call.

Stilted answered 23/1, 2009 at 13:27 Comment(1)
Thank you for your reply. Good suggestion regarding the creation of the Callbacks. I have tried it, but the handle/thread/context switch rate still increases, although it does appear to increase at a slower rate, so an improvement.Whimper
C
4

No need at all for BeginWrite. You only send 3 bytes, they'll easily fit in the transmit buffer, and you're always sure that the buffer is empty when you send the next set.

Keep in mind that serial ports are much slower than TCP/IP connections. It is pretty likely that you end up calling BeginRead() for every single byte that you receive. That gives the thread pool a good workout, you'd definitely see a lot of context switches. Not so sure about handle consumption. Be sure to test that without the debugger attached.

Trying DataReceived instead of BeginRead() is definitely something you should try. Pull instead of push, you'll use a threadpool thread when there's something happening instead of always having one active.

Coniah answered 24/1, 2009 at 4:2 Comment(2)
I take the point with regard to BeginWrite, it is not really necessary. BeginRead doesn't trigger for every byte, but it does trigger pretty frequently. Maybe I could use DataReceived and pass the bytes into a stream of my own to keep the class consistent? I am testing a 'Release' version too, BTW.Whimper
@Hans: As Andy said, BeginRead/EndRead easily handles multiple bytes per call (depending on the timeout setting). And S.IO.P.SerialPort blocks a threadpool thread at all times in order to detect data and fire DataReceived, so you're imagining the lower resource usage. In reality, there are fewer kernel calls needed for BeginRead/EndRead than with the detect-activity-then-read solution.Chambless
I
0

Is it possible to take the data comming in from the serial port and directly send it to a file? At high a baud rates (1 MegaBaud) it is difficult to handle this amount of non-stop data.

Inosculate answered 26/3, 2009 at 20:13 Comment(0)
K
0

Is the response from the device always a fixed size? If so, try using SerialPort.Read and pass the packet size. This will block, so combine it with DataReceived. Better yet, if the response always ends with the same character(s), and this ending signature is guaranteed to be unique in the packet, set the NewLine property and use ReadLine. This will immunize you against future packet size changes.

Korey answered 30/5, 2011 at 20:36 Comment(5)
The packet sizes are variable unfortunately.Whimper
Bummer. So how do you know when you've received a full packet?Korey
The packet contains the header and terminator bytes, with length of packet and checksum encoded within it.Whimper
If the terminator is guaranteed to not be used within the payload, you can set NewLine and use ReadLine.Korey
Unfortunately (again!) the packet contains binary byte data, not ASCII, and therefore all values from 0x0 to 0xFF are valid. Thank you for your suggestions though.Whimper

© 2022 - 2024 — McMap. All rights reserved.