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

C#中读取SerialPort时如何避免线程泄漏?

C#中读取SerialPort时如何避免线程泄漏?

看起来你碰到了两个棘手的问题:一是SerialPort的DataReceived事件触发后,线程池线程数量持续上涨,甚至能到2万+;二是不得不靠Thread.Sleep(50)来凑齐完整的串口响应,总觉得这方法不靠谱。我来一步步帮你解决这两个问题。

一、先搞定线程泄漏的核心原因

你怀疑Thread.Sleep(50)是问题根源,其实猜得没错——但不是因为线程没返回线程池,而是Sleep会阻塞线程池线程,导致线程池无法及时复用这些线程,只能不断新建线程。

线程池的线程本来是用来处理短任务的,你每次在DataReceived里让线程睡50ms,相当于把这个线程“占着茅坑不拉屎”。当串口数据频繁触发事件时,线程池里可用的线程很快被占满,它就会新建更多线程来处理新的请求。虽然这些线程最后会被回收,但回收有延迟,长期积累下来,线程数自然就蹭蹭往上涨了。

所以解决线程泄漏的关键,就是去掉那个Sleep,改用非阻塞的方式读取完整数据——这刚好也能解决你第二个头疼的问题。

二、不用Sleep,怎么可靠读取完整的串口数据?

串口数据是流式的,DataReceived事件可能只触发了一部分数据,所以不能每次触发就读取所有现有数据就完事,得自己维护缓冲区,直到凑齐一整帧有效数据。具体怎么做,取决于你的设备通信协议:

方案1:根据结束符判断(比如CRLF)

如果你的设备返回的数据是以固定结束符(比如你代码里的sgCRLF)结尾的,那可以这样处理:

  1. 给每个SerialPort关联一个私有缓冲区(注意线程安全,因为DataReceived是在后台线程触发的)
  2. 每次DataReceived触发时,把读取到的字符追加到缓冲区
  3. 检查缓冲区里是否包含结束符,如果包含,就从缓冲区里提取从开头到结束符的完整帧,剩下的留在缓冲区里等待后续数据

举个简化的例子:

// 给每个串口维护一个线程安全的缓冲区
private readonly Dictionary<SerialPort, StringBuilder> _portBuffers = new Dictionary<SerialPort, StringBuilder>();

private void SerialDataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
    SerialPort sp = (SerialPort)sender;
    if (sp == null || !sp.IsOpen) return;

    // 确保当前串口有对应的缓冲区
    lock (_portBuffers)
    {
        if (!_portBuffers.ContainsKey(sp))
        {
            _portBuffers[sp] = new StringBuilder();
        }
    }

    try
    {
        // 读取当前可用的所有数据
        string newData = sp.ReadExisting();
        lock (_portBuffers[sp])
        {
            _portBuffers[sp].Append(newData);
        }

        // 检查是否有完整的帧(这里假设结束符是CRLF)
        ProcessCompleteFrames(sp);
    }
    catch (IOException ex)
    {
        // 异常处理逻辑...
        SerialPort port = (SerialPort)sender;
        string msg = $"[ERROR] SerialDataReceived {port?.PortName ?? "[unknown port]"} - {ex.Message}";
        Debug.WriteLine(msg);
        TDUtils.WriteLog(msg);
    }
}

private void ProcessCompleteFrames(SerialPort sp)
{
    StringBuilder buffer = _portBuffers[sp];
    lock (buffer)
    {
        string endMarker = "\r\n"; // 替换成你实际的结束符
        int endIndex;
        while ((endIndex = buffer.ToString().IndexOf(endMarker)) != -1)
        {
            // 提取完整的帧
            string fullFrame = buffer.ToString().Substring(0, endIndex + endMarker.Length);
            // 移除已处理的帧
            buffer.Remove(0, endIndex + endMarker.Length);

            // 在这里处理你的完整帧数据(就是你原来在Sleep之后做的逻辑)
            HandleReceivedFrame(fullFrame, sp);
        }
    }
}

private void HandleReceivedFrame(string fullFrame, SerialPort sp)
{
    Scales? scales = GetScalesFromSerialPort(sp);
    if (scales == null) return;

    int dataLen = fullFrame.Length;
    bool isC320 = scales.isC320;

    // 原来的帧判断和处理逻辑直接搬过来就行
    if (dataLen >= 17 && fullFrame.StartsWith(isC320 ? Scales.C300_RECEIVED_GROSS : Scales.R300_RECEIVED_GROSS))
    {
        string hexWeight = fullFrame.Substring(9, 8);
        if (int.TryParse(hexWeight, System.Globalization.NumberStyles.HexNumber, null, out int weightInt))
        {
            UpdateWeight(scales.raceNumber, (double)weightInt / 10d);
        }
        else
        {
            Debug.WriteLine($"Scales ({scales.raceNumber}) weight data: (invalid)");
        }
    }
    else if (dataLen >= 15 && fullFrame.StartsWith(isC320 ? Scales.C300_RECEIVED_ZERO : Scales.R300_RECEIVED_ZERO))
    {
        Debug.WriteLine($"[DEBUG] Read Zero from race {scales.raceNumber} dataLen: {dataLen}");
    }
    else
    {
        Debug.WriteLine($"[DATA] (unknown) {fullFrame} from serialport on race {scales.raceNumber}");
    }
}

方案2:根据帧长度判断

如果你的设备返回的每帧数据长度是固定的,或者可以通过帧头里的字段获取长度,那可以:

  1. 同样维护缓冲区
  2. 先读取足够的字节数来获取帧长度
  3. 等缓冲区里的数据长度达到帧长度时,再提取完整帧处理

额外注意事项

  • 线程安全:因为DataReceived是在后台线程触发的,操作缓冲区时一定要加锁(或者用线程安全的集合,比如ConcurrentQueue),避免多线程同时修改导致数据错乱。
  • 避免重复订阅事件:看你的Init方法里每次都先-=再+=事件处理,这没问题,但要确保不会多次调用Init导致不必要的操作。
  • 线程池线程的回收:当你去掉Sleep后,DataReceived事件处理会变成短任务,线程池线程处理完就会返回池里复用,不会再被长时间占用,线程数自然就稳定下来了。

三、其他可能的线程泄漏排查点

如果去掉Sleep后线程数还是上涨,那可以检查:

  • 有没有在其他地方(比如BeginWeighing或者UpdateWeight)创建了新线程却没正确回收?
  • 是不是串口设备频繁发送数据,导致DataReceived触发太频繁?但只要处理逻辑是短任务,线程池应该能应付。

按照上面的方法改完,你应该能看到线程数稳定下来,而且不用再靠Sleep来凑数据了。

备注:内容来源于stack exchange,提问作者smstrong

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 10:49:29