3
\$\begingroup\$

I'm having an implementation using SerialPort in C# using Visual Studio 2019. I'm using it in a Windows Application, using .NET Framework 4.7.2.

When I open and initialize the SerialPort, I'm launching 2 "message pump tasks", one for receiving data and one for transmitting data.

The SerialPort has a ConcurrentQueue to store the commands to transmit, and empties that queue if it has commands available. If there are no more commands, the transmitting pump does nothing.

Each command sent to the external device will return an acknowledge (simply the string "A\n"). Other received data is considered as a command coming from the external device, and is consolidated in a shared ConcurrentQueue that stores all the received commands, ready to be processed by the main application.

My question is:

What is better:

  • Using the asynchronous versions BaseStream.ReadAsync and BaseStream.WriteAsync
  • Using the synchronous versions BaseStream.Read and BaseStream.Write

Below are the 2 versions of my code. I don't see any difference in performance, and because the message pumps run in the background in both versions, I don't have the feeling that my UI thread is "blocking".

Synchronous version:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace SerialPortTest
{
    class COMDevice
    {
        private readonly byte[] LF = { (byte)'\n' };

        public List<string> _RegisteredCmds = new List<string>();

        private SerialPort _serialPort = new SerialPort();

        private string _DeviceName;
        private string _ProcessorType;

        private ConcurrentQueue<string> _RxQueue;
        public ConcurrentQueue<string> _TxQueue = new ConcurrentQueue<string>();

        private Task _RxPump;
        private Task _TxPump;

        private ManualResetEvent mreAck = new ManualResetEvent(false);

        public bool IsOpen { get { return _serialPort.IsOpen; } }

        public COMDevice(string portName, ConcurrentQueue<string> RxQueue, Int32 baudRate = 500000)
        {
            _serialPort.PortName = portName;
            _serialPort.BaudRate = baudRate;
            _RxQueue = RxQueue;

            _serialPort.Handshake = Handshake.None;

            // format 8-N-1
            _serialPort.DataBits = 8;
            _serialPort.Parity = Parity.None;
            _serialPort.StopBits = StopBits.One;

            _serialPort.NewLine = "\n";

            _serialPort.ReadTimeout = 100;
            _serialPort.WriteTimeout = 100;
        }

        public bool Open()
        {
            try
            {
                _serialPort.Open();
                return true;
            }
            catch (TimeoutException)
            {
                if (_serialPort.IsOpen)
                    _serialPort.Close();
                return false;
            }
            catch (Exception)
            {
                if (_serialPort.IsOpen)
                    _serialPort.Close();
                return false;
            }
        }

        public bool Close()
        {
            if (!_serialPort.IsOpen)
                return false;

            try
            {
                _serialPort.Write("RESET\n");
                _serialPort.Close();

                Task[] tasks = { _TxPump, _RxPump };
                Task.WaitAll(tasks, 100);

                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        private void ClearInputBuffer()
        {
            while (true)
            {
                int readCount = _serialPort.BytesToRead;
                if (readCount == 0)
                    break;
                byte[] buffer = new byte[readCount];
                _serialPort.Read(buffer, 0, readCount);
            }
        }
        
        public bool Initialize()
        {
            try
            {
                // clean receive buffer of Arduino and this application
                _serialPort.Write("\n");
                ClearInputBuffer();

                // get identification
                Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} Initialize: Write IDENT"); 
                _serialPort.Write("IDENT\n");

                _DeviceName = _serialPort.ReadLine();
                _ProcessorType = _serialPort.ReadLine();
                Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} Initialize: DeviceName: \"{_DeviceName}\" ProcessorType: \"{_ProcessorType}\"");

                // register the LED/Data commands
                _serialPort.Write("REGISTER\n");
                while (true)
                {
                    string sRegister = _serialPort.ReadLine();

                    if (sRegister == "")
                        break;
                    _RegisteredCmds.Add(sRegister);
                }
                Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} Initialize: {_RegisteredCmds.Count} Commands Registered");

                // start transmitting and receiving commands
                _TxPump = Task.Run(() => TxPump());
                _RxPump = Task.Run(() => RxPump());

                return true;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} Initialize: Exception {ex}");
                return false;
            }
        }

        private void RxPump()
        {
            var buffer = new byte[1024];
            StringBuilder sbCmd = new StringBuilder("", 256);

            try
            {
                while (_serialPort.IsOpen)
                {
                    if (_serialPort.BytesToRead == 0)
                        continue;

                    int cnt = _serialPort.BaseStream.Read(buffer, 0, 1024);

                    for (int i = 0; i < cnt; i++)
                    {
                        if ((char)buffer[i] == '\n')
                        {
                            if ((sbCmd.Length == 1) && (sbCmd[0] == 'A'))
                                // ACK sequence received
                                mreAck.Set();
                            else if (sbCmd.Length != 0)
                                // Command received
                                _RxQueue.Enqueue(sbCmd.ToString());
                            sbCmd.Clear();
                        }
                        else
                            sbCmd.Append((char)buffer[i]);
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} RxCommandsAsync: Exception {ex}");
            }
        }

        private void TxPump()
        {
            try
            {
                while (_serialPort.IsOpen)
                {
                    string sCmd;

                    if (_TxQueue.Count == 0)
                        continue;
                    else
                        _TxQueue.TryDequeue(out sCmd);

                    byte[] buffer = Encoding.ASCII.GetBytes($"{sCmd}\n");

                    int attempts = 2;

                    while (attempts-- != 0)
                    {
                        _serialPort.BaseStream.Write(buffer, 0, sCmd.Length + 1);

                        // Reset the ManualResetEvent and wait for ACK for 50 msec
                        mreAck.Reset();
                        if (mreAck.WaitOne(50))
                            break;
                        else
                        {
                            Debug.WriteLine($"Not Ack {2 - attempts} for \"{sCmd}\"");
                            // Send linefeed to be sure we are in sync
                            _serialPort.BaseStream.Write(LF, 0, 1);
                        }
                    }
                }
            }
            catch (Exception)
            {
                Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} TxCommandsAsync: Exception");
            }
        }
    }
}

Asynchronous version:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace SerialPortTest
{
    class COMDevice
    {
        private readonly byte[] LF = { (byte)'\n' };

        public List<string> _RegisteredCmds = new List<string>();

        private SerialPort _serialPort = new SerialPort();

        private string _DeviceName;
        private string _ProcessorType;

        private ConcurrentQueue<string> _RxQueue;
        public ConcurrentQueue<string> _TxQueue = new ConcurrentQueue<string>();

        private Task _RxPump;
        private Task _TxPump;

        private ManualResetEvent mreAck = new ManualResetEvent(false);

        public bool IsOpen { get { return _serialPort.IsOpen; } }

        public COMDevice(string portName, ConcurrentQueue<string> RxQueue, Int32 baudRate = 500000)
        {
            _serialPort.PortName = portName;
            _serialPort.BaudRate = baudRate;
            _RxQueue = RxQueue;

            _serialPort.Handshake = Handshake.None;

            // format 8-N-1
            _serialPort.DataBits = 8;
            _serialPort.Parity = Parity.None;
            _serialPort.StopBits = StopBits.One;

            _serialPort.NewLine = "\n";

            _serialPort.ReadTimeout = 100;
            _serialPort.WriteTimeout = 100;
        }

        public bool Open()
        {
            try
            {
                _serialPort.Open();
                return true;
            }
            catch (TimeoutException)
            {
                if (_serialPort.IsOpen)
                    _serialPort.Close();
                return false;
            }
            catch (Exception)
            {
                if (_serialPort.IsOpen)
                    _serialPort.Close();
                return false;
            }
        }

        public bool Close()
        {
            if (!_serialPort.IsOpen)
                return false;

            try
            {
                _serialPort.Write("RESET\n");
                _serialPort.Close();

                Task[] tasks = { _TxPump, _RxPump };
                Task.WaitAll(tasks, 100);

                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        private void ClearInputBuffer()
        {
            while (true)
            {
                int readCount = _serialPort.BytesToRead;
                if (readCount == 0)
                    break;
                byte[] buffer = new byte[readCount];
                _serialPort.Read(buffer, 0, readCount);
            }
        }
        
        public bool Initialize()
        {
            try
            {
                // clean receive buffer of Arduino and this application
                _serialPort.Write("\n");
                ClearInputBuffer();

                // get identification
                Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} Initialize: Write IDENT"); 
                _serialPort.Write("IDENT\n");

                _DeviceName = _serialPort.ReadLine();
                _ProcessorType = _serialPort.ReadLine();
                Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} Initialize: DeviceName: \"{_DeviceName}\" ProcessorType: \"{_ProcessorType}\"");

                // register the LED/Data commands
                _serialPort.Write("REGISTER\n");
                while (true)
                {
                    string sRegister = _serialPort.ReadLine();

                    if (sRegister == "")
                        break;
                    _RegisteredCmds.Add(sRegister);
                }
                Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} Initialize: {_RegisteredCmds.Count} Commands Registered");

                // start transmitting and receiving commands
                _TxPump = Task.Run(() => TxPumpAsync());
                _RxPump = Task.Run(() => RxPumpAsync());

                return true;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} Initialize: Exception {ex}");
                return false;
            }
        }

        private async Task RxPumpAsync()
        {
            var buffer = new byte[1024];
            StringBuilder sbCmd = new StringBuilder("", 256);

            try
            {
                while (_serialPort.IsOpen)
                {
                    int cnt = await _serialPort.BaseStream.ReadAsync(buffer, 0, 1024);

                    for (int i = 0; i < cnt; i++)
                    {
                        if ((char)buffer[i] == '\n')
                        {
                            if ((sbCmd.Length == 1) && (sbCmd[0] == 'A'))
                                // ACK sequence received
                                mreAck.Set();
                            else if (sbCmd.Length != 0)
                                // Command received
                                _RxQueue.Enqueue(sbCmd.ToString());
                            sbCmd.Clear();
                        }
                        else
                            sbCmd.Append((char)buffer[i]);
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} RxCommandsAsync: Exception {ex}");
            }
        }

        private async Task TxPumpAsync()
        {
            try
            {
                while (_serialPort.IsOpen)
                {
                    string sCmd;

                    if (_TxQueue.Count == 0)
                        continue;
                    else
                        _TxQueue.TryDequeue(out sCmd);

                    byte[] buffer = Encoding.ASCII.GetBytes($"{sCmd}\n");

                    int attempts = 2;

                    while (attempts-- != 0)
                    {
                        await _serialPort.BaseStream.WriteAsync(buffer, 0, sCmd.Length + 1);

                        // Reset the ManualResetEvent and wait for ACK for 50 msec
                        mreAck.Reset();
                        if (mreAck.WaitOne(50))
                            break;
                        else
                        {
                            Debug.WriteLine($"Not Ack {2 - attempts} for \"{sCmd}\"");
                            // Send linefeed to be sure we are in sync
                            await _serialPort.BaseStream.WriteAsync(LF, 0, 1);
                        }
                    }
                }
            }
            catch (Exception)
            {
                Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss:fff")} TxCommandsAsync: Exception");
            }
        }
    }
}
\$\endgroup\$
4
  • 1
    \$\begingroup\$ If the sync version is acceptable from performance wise then why do you bother with the async one? \$\endgroup\$ Commented Mar 28, 2022 at 8:30
  • \$\begingroup\$ @PeterCsala Well, maybe I overlooked something, which is the reason for my question. But you are probably right, and I shouldn't bother. \$\endgroup\$ Commented Mar 28, 2022 at 10:25
  • \$\begingroup\$ By the way async can provide you higher throughput not better performance. So do not expect that async code will run faster. \$\endgroup\$ Commented Mar 28, 2022 at 10:34
  • \$\begingroup\$ Not really a code comment; but instead of using Queues, you can also use Channels See this example project. \$\endgroup\$ Commented Dec 29, 2022 at 10:33

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.