You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 7 C# SerialPort信号量超时过期问题的解决与规避

虚拟串口操作IOException:信号量超时时间已过期问题解决

问题现象

在C# ASP.NET Core 7 Web API项目中操作虚拟串口读写时,偶尔会触发IOException,错误提示为**“信号量超时时间已过期”**,且出现该错误后,除非重启连接设备,否则SerialPort似乎无法恢复。

异常栈追踪

System.IO.IOException: The semaphore timeout period has expired. : 'COM7'
at System.IO.Ports.SerialStream.EndWrite(IAsyncResult asyncResult)
at System.IO.Ports.SerialStream.Write(Byte[] array, Int32 offset, Int32 count, Int32 timeout)
at System.IO.Ports.SerialPort.Write(Byte[] buffer, Int32 offset, Int32 count)

当前代码实现

SerialPort实例化代码

if (_serialPort == null && SerialPort.GetPortNames().Contains(poortNaam))
{
    _serialPort = new SerialPort(_poortNaam, _baudrate, _parity, _dataBits, _stopBits);
    _serialPort.WriteTimeout = 500;
    _serialPort.ReadTimeout = 500;
    _serialPort.ReadBufferSize = 8192;
    _serialPort.WriteBufferSize = 8192;
    _serialPort.RtsEnable = true;

    Semaphore.Wait();

    if (_serialPort.IsOpen)
    {
        _serialPort.Close();
        Thread.Sleep(250);
    }

    _serialPort.Open();

    Thread.Sleep(500);

    Semaphore.Release();
}

写入方法逻辑

try
{
     _serialPort.DiscardInBuffer();
     _serialPort.DiscardOutBuffer();

     if (!_serialPort.IsOpen)
     {
         _serialPort.Open();
         Thread.Sleep(250);
     }

     Trace.Write("Verstuur: ");

     foreach (var item in frame)
     {
         Trace.Write($"{item},");
     }

     _serialPort.Write(frame, 0, frame.Length);
 }
 catch (IOException ioe)
 {
     Trace.WriteLine(ioe.ToString());
     _serialPort.Close();
     Thread.Sleep(500);
     _serialPort.Open();
     Thread.Sleep(500);
     _serialPort.Write(frame, 0, frame.Length);
 }

读取方法逻辑

byte incomingByte;

while(!timeout && !MessageStop)
{
    try
    {
        int IncomingBytes = _serialPort.BytesToRead;

        for (int i = 0; i < IncomingBytes ; i++)
        { 
            incomingByte = (byte)_serialPort.ReadByte();
            IncomingBytesList.Add(incomingByte );

            if (incomingByte == Ex || (incomingByte == Nak && IncomingBytes == 1) || (incomingByte == Ak && IncomingBytes == 1))
            {
                MessageStop = true;
                break;
            }
        }
    }
    catch (IOException ioe)
    {
        Trace.WriteLine(ioe.Message);
        _serialPort.Close();
        Thread.Sleep(100);
        _serialPort.Open();
        Thread.Sleep(200);
    }
}

已尝试的方案

  • 使用SemaphoreSlim限制线程访问代码
  • 在串口开闭操作间添加延迟
  • 尝试重新实例化SerialPort对象

需求

推测问题由SerialPort辅助线程卡住导致超时,需解决:

  1. 如何最佳处理/恢复该问题?
  2. 如何避免触发该问题?

解决方案

一、问题处理与恢复优化

  1. 彻底销毁旧实例,重新创建SerialPort
    出现超时异常后,直接关闭旧实例并释放资源,不要复用,重新创建新的SerialPort对象。因为原实例的内部辅助线程可能已经卡死,复用会导致问题持续。修改异常处理逻辑:

    catch (IOException ioe)
    {
        Trace.WriteLine(ioe.ToString());
        // 彻底释放旧实例资源
        if (_serialPort != null)
        {
            try { _serialPort.Close(); } catch {}
            _serialPort.Dispose();
            _serialPort = null;
        }
        Thread.Sleep(1000);
        // 重新初始化串口
        InitializeSerialPort(); // 调用你的实例化逻辑方法
        // 重试写入/读取
        if (_serialPort != null && _serialPort.IsOpen)
        {
            _serialPort.Write(frame, 0, frame.Length);
        }
    }
    
  2. 添加重试次数限制
    不要无限重试,设置最大重试次数(比如3次),避免陷入死循环:

    int retryCount = 0;
    const int maxRetries = 3;
    bool writeSuccess = false;
    while (!writeSuccess && retryCount < maxRetries)
    {
        try
        {
            // 写入逻辑
            _serialPort.Write(frame, 0, frame.Length);
            writeSuccess = true;
        }
        catch (IOException ioe)
        {
            retryCount++;
            Trace.WriteLine($"写入失败,重试第{retryCount}次:{ioe.Message}");
            // 销毁并重建串口
            // ... 上述销毁重建代码 ...
        }
    }
    if (!writeSuccess)
    {
        Trace.WriteLine("写入重试多次失败,请检查设备状态");
        // 触发告警或记录错误日志
    }
    

二、避免触发问题的优化措施

  1. 避免频繁开关串口
    当前代码中写入方法每次都检查是否打开并可能重新打开,频繁的开闭会增加线程冲突和资源泄漏风险。建议串口初始化后保持打开状态,除非明确检测到连接异常再关闭重建。

  2. 优化线程同步机制
    原代码中Semaphore的使用存在问题:在实例化时获取信号量,但如果初始化过程中抛出异常,可能导致信号量无法释放。改用SemaphoreSlim并配合try/finally确保释放:

    private readonly SemaphoreSlim _serialSemaphore = new SemaphoreSlim(1, 1);
    
    // 在初始化或操作串口时
    await _serialSemaphore.WaitAsync();
    try
    {
        // 串口操作逻辑:打开、读写等
    }
    finally
    {
        _serialSemaphore.Release();
    }
    
  3. 避免同步读写阻塞主线程
    ASP.NET Core是异步框架,同步的SerialPort读写会阻塞线程池线程,增加线程竞争风险。改用异步读写方式,或者将串口操作放到单独的后台线程/Task中执行:

    // 异步写入示例
    public async Task WriteAsync(byte[] frame)
    {
        await _serialSemaphore.WaitAsync();
        try
        {
            if (!_serialPort.IsOpen)
            {
                _serialPort.Open();
                await Task.Delay(250);
            }
            _serialPort.DiscardInBuffer();
            _serialPort.DiscardOutBuffer();
            // 模拟异步写入(SerialPort本身无原生Async,可包装成Task)
            await Task.Run(() => _serialPort.Write(frame, 0, frame.Length));
        }
        finally
        {
            _serialSemaphore.Release();
        }
    }
    
  4. 调整超时时间与缓冲区设置
    当前500ms的超时时间对于虚拟串口可能过短,可适当调整为1000ms。同时,避免频繁调用DiscardInBuffer和DiscardOutBuffer,除非明确需要清除缓存,否则会打断正常的数据传输流程。

  5. 监控串口状态
    定期检查串口连接状态,比如通过发送心跳包确认设备是否在线,一旦检测到异常立即销毁并重建串口,避免问题积累。


内容的提问来源于stack exchange,提问作者Kasper

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 08:31:15