C# 串口关闭时主界面卡死原因分析
问题描述
前几天用serialport类写一个串口的测试程序,关闭串口的时候会让界面卡死。
参考博客,得出界面卡死原因:主线程和其他的线程由于资源或者锁争夺,出现了死锁。
参考知乎文章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源码,发现这种解决问题的方法还是很有用处的。结果不重要,解决问题的方法是最重要的。
上一篇: 用Photoshop制作漂亮的海报
下一篇: 中风的三大饮食禁忌 中风的饮食原则
推荐阅读