C# 串口关闭时主界面卡死原因分析

您所在的位置:网站首页 设备串口很卡 C# 串口关闭时主界面卡死原因分析

C# 串口关闭时主界面卡死原因分析

2024-07-18 08:18:16| 来源: 网络整理| 查看: 265

问题描述#

前几天用SerialPort类写一个串口的测试程序,关闭串口的时候会让界面卡死。 参考博客windows程序界面卡死的原因,得出界面卡死原因:主线程和其他的线程由于资源或者锁争夺,出现了死锁。

参考知乎文章WinForm界面假死,如何判断其卡在代码中的哪一步?,通过点击调试暂停,查看ui线程函数栈,直接定位阻塞代码的行数,确定问题出现在SerialPort类的Close()方法。

参考文章C# 串口操作系列(2) -- 入门篇,为什么我的串口程序在关闭串口时候会死锁 ?文章的解决方法和网上的大部分解决方法类似:定义2个bool类型的标记Listening和Closing,关闭串口和接受数据前先判断一下。我个人并不太接受这种方法,感觉还有更好的方式,而且文章讲述的也并不太清楚。

查找原因#

基于刨根问底的原则,我继续查找问题发生的原因。 先看看导致界面卡死的代码:

void comm_DataReceived(object sender, SerialDataReceivedEventArgs e) { //获取串口读取的字节数 int n = comm.BytesToRead; //读取缓冲数据 comm.Read(buf, 0, n); //因为要访问ui资源,所以需要使用invoke方式同步ui。 this.Invoke(new Action(() =>{...界面更新,略})); } private void buttonOpenClose_Click(object sender, EventArgs e) { //根据当前串口对象,来判断操作 if (comm.IsOpen) { //打开时点击,则关闭串口 comm.Close();//界面卡死的原因 } else {...} }

问题就出现在上面的代码中,原理目前还不明确,我只能参考.NET源码来查找问题。

SerialPort类Open()方法#

SerialPort类Close()方法的源码如下:

public void Open() { //省略部分代码... internalSerialStream = new SerialStream(portName, baudRate, parity, dataBits, stopBits, readTimeout, writeTimeout, handshake, dtrEnable, rtsEnable, discardNull, parityReplace); internalSerialStream.SetBufferSizes(readBufferSize, writeBufferSize); internalSerialStream.ErrorReceived += new SerialErrorReceivedEventHandler(CatchErrorEvents); internalSerialStream.PinChanged += new SerialPinChangedEventHandler(CatchPinChangedEvents); internalSerialStream.DataReceived += new SerialDataReceivedEventHandler(CatchReceivedEvents); }

每次执行SerialPort类Open()方法都会出现实例化一个SerialStream类型的对象,并将CatchReceivedEvents事件处理程序绑定到SerialStream实例的DataReceived事件。

SerialStream类CatchReceivedEvents方法的源码如下:

private void CatchReceivedEvents(object src, SerialDataReceivedEventArgs e) { SerialDataReceivedEventHandler eventHandler = DataReceived; SerialStream stream = internalSerialStream; if ((eventHandler != null) && (stream != null)){ lock (stream) { bool raiseEvent = false; try { raiseEvent = stream.IsOpen && (SerialData.Eof == e.EventType || BytesToRead >= receivedBytesThreshold); } catch { // Ignore and continue. SerialPort might have been closed already! } finally { if (raiseEvent) eventHandler(this, e); // here, do your reading, etc. } } } }

可以看到SerialStream类CatchReceivedEvents方法触发自身的DataReceived事件,这个DataReceived事件就是我们处理串口接收数据的用到的事件。

DataReceived事件处理程序是在lock (stream) {...}块中执行的,ErrorReceived 、PinChanged 也类似。

SerialPort类Close()方法#

SerialPort类Close()方法的源码如下:

// Calls internal Serial Stream's Close() method on the internal Serial Stream. public void Close() { Dispose(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected override void Dispose( bool disposing ) { if( disposing ) { if (IsOpen) { internalSerialStream.Flush(); internalSerialStream.Close(); internalSerialStream = null; } } base.Dispose( disposing ); }

可以看到,执行Close()方法最终会调用Dispose( bool disposing )方法。 微软SerialPort类对父类的Dispose( bool disposing )方法进行了重写,在执行base.Dispose( disposing )前会执行internalSerialStream.Close()方法,也就是说SerialPort实例执行Close()方法时会先关闭SerialPort实例内部的SerialStream实例,再执行父类的Close()操作。

base.Dispose( disposing )方法不作为重点,我们再看internalSerialStream.Close()方法。

SerialStream类源码没有找到Close()方法,说明没有重写父类的Close方法,直接看父类的Close()方法,源码如下:

public virtual void Close() { Dispose(true); GC.SuppressFinalize(this); }

SerialStream父类的Close方法调用了Dispose(true),不过SerialStream类重写了父类的Dispose(bool disposing)方法,源码如下:

protected override void Dispose(bool disposing) { if (_handle != null && !_handle.IsInvalid) { try { //省略一部分代码 } finally { // If we are disposing synchronize closing with raising SerialPort events if (disposing) { lock (this) { _handle.Close(); _handle = null; } } else { _handle.Close(); _handle = null; } base.Dispose(disposing); } } }

SerialStream父类的Close方法调用了Dispose(true),上面的代码一定会执行到lock (this) 语句,也就是说SerialStream实例执行Close()方法时会lock自身。

死锁原因#

把我们前面源码分析的结果总结一下:

DataReceived事件处理程序是在lock (stream) {...}块中执行的SerialPort实例执行Close()方法时会先关闭SerialPort实例内部的SerialStream实例SerialStream实例执行Close()方法时会lock实例自身

当辅助线程调用DataReceived事件处理程序处理串口数据但还未更新界面时,点击界面“关闭”按钮调用SerialPort实例的Close()方法,UI线程会在lock(stream)处一直等待辅助线程释放stream的线程锁。 当辅助线程处理完数据准备更新界面时问题来了,DataReceived事件处理程序中的this.Invoke()一直会等待UI线程来执行委托,但此时UI线程还停在SerialPort实例的Close()方法处等待DataReceived事件处理程序执行完成。 此时,线程死锁发生,两边都执行不下去了。

解决死锁#

网上大多数方法都是定义2个bool类型的标记Listening和Closing,关闭串口和接受数据前先判断一下。 我的方法是DataReceived事件处理程序用this.BeginInvoke()更新界面,不等待UI线程执行完委托就返回,stream的线程锁会很快释放,SerialPort实例的Close()方法也无需等待。

总结#

问题最终的答案其实很简单,但我在查阅.NET源码查找问题源头的过程中收获了很多。这是我第一次这么深入的查看.NET源码,发现这种解决问题的方法还是很有用处的。结果不重要,解决问题的方法是最重要的。  

发现问题 本人发现在页面资源释放时(Disposed事件中) 没有在页面关闭前调用System.IO.Ports.SerialPort的Close方法 调用System.IO.Ports.SerialPort的Close方法大概率会出现界面卡死 推测问题原因 发现问题经过 这边应用COM的场景是读取传感器重量实时显示到dataGridView上以及某些业务处理 也就是在页面关闭前是不可能关闭COM口的即不会调用System.IO.Ports.SerialPort的Close方法 随后发现界面卡死了 推测问题原因 COM口在等待UI线程执行某些操作 UI线程又在等待COM口进行关闭 这样产生的死锁导致界面卡死 解决问题思路 推测问题既然是双方都在等待导致的 那解决问题的思路就是先等一方的逻辑先执行完成即可 解决问题 COM帮助类中写两个方法 这两个方法中主要通过AllowDisposeFunc委托的返回值 判断UI线程是否执行完成         ///         /// 异步进行循环等待,直到超时或允许释放资源.         /// 如果允许释放资源则关闭com口,释放资源         ///         /// 指示什么情况下允许释放资源         /// 超时时间,≤0永不超时(毫秒)         /// 是否成功释放资源         public Task DisposeAsync(Func AllowDisposeFunc, int TimeOut = -1)         {             return Task.Run(() => Dispose(AllowDisposeFunc, TimeOut));         }

        ///         /// 进行循环等待,直到超时或允许释放资源.         /// 如果允许释放资源则关闭com口,释放资源         ///         /// 指示什么情况下允许释放资源         /// 超时时间,≤0永不超时(毫秒)         /// 是否成功释放资源         public bool Dispose(Func AllowDisposeFunc, int TimeOut = -1)         {             if (!Com.IsOpen)             {                 Dispose();                 return true;             }

            int time = 50;             int timeSum = 0;             while (!AllowDisposeFunc())             {                 timeSum += time;                 System.Threading.Thread.Sleep(time);                 if (TimeOut > 0 && timeSum >= TimeOut)                 {                     return false;                 }             }             Dispose();             return true;         } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 下面代码中的this是Form,dataGridView_Info是DataGridView 本人的问题是页面关闭释放资源时同时调用COM口的关闭事件出现死锁 写法1:理论上可能会有出现死锁(我没碰到)         ///         /// 首次启动时触发         ///         protected virtual void FirstStart()         {             CreateComHelper();             this.Disposed += (sender, e) =>             {                 ComHelper.Dispose(() => dataGridView_Info.IsDisposed);             };         } 1 2 3 4 5 6 7 8 9 10 11 一定会出现问题的写法: 出现问题原因dataGridView_Info.Disposed事件中 dataGridView_Info.IsDisposed一定是false 也就是说COM等不到UI线程的结束         ///         /// 首次启动时触发         ///         protected virtual void FirstStart()         {             CreateComHelper();             dataGridView_Info.Disposed += (sender, e) =>             {                 ComHelper.Dispose(() => dataGridView_Info.IsDisposed);             };         } 1 2 3 4 5 6 7 8 9 10 11 写法2:理论上不会出现死锁 因为异步了,不管是否将this替换为了dataGridView_Info都不会影响UI线程的运行,等UI线程执行完毕后,异步的线程就能读到IsDisposed=true了,此时COM就可以关闭成功了         ///         /// 首次启动时触发         ///         protected virtual void FirstStart()         {             CreateComHelper();             this.Disposed += (sender, e) =>             {                 ComHelper.DisposeAsync(() => dataGridView_Info.IsDisposed);             };         }  



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


    图片新闻

    实验室药品柜的特性有哪些
    实验室药品柜是实验室家具的重要组成部分之一,主要
    小学科学实验中有哪些教学
    计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
    实验室各种仪器原理动图讲
    1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
    高中化学常见仪器及实验装
    1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
    微生物操作主要设备和器具
    今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
    浅谈通风柜使用基本常识
     众所周知,通风柜功能中最主要的就是排气功能。在

    专题文章

      CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭