C# 串口关闭时主界面卡死原因分析
问题描述
前几天用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源码,发现这种解决问题的方法还是很有用处的。结果不重要,解决问题的方法是最重要的。
C# 串口关闭时主界面卡死原因分析的更多相关文章
- winform 防止主界面卡死
总结网络上的解决方案:新线程=> 委托=> 主界面的异步更新方法(IAsyncResult BeginInvoke(Delegate method)),一句话就是通过委托调用另一个线程的异 ...
- MySQL在删除表时I/O错误原因分析
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由腾讯数据库技术 发表于云+社区专栏 问题现象 最近使用sysbench测试MySQL,由于测试时间较长,写了一个脚本按prepare- ...
- SQL Server扩展事件的使用ring_buffer target时“丢失”事件的原因分析以及ring_buffer target潜在的问题
事情起因: 排查SQL Server上的死锁问题,一开始想到的就是扩展事件, 第一种方案,开profile守株待兔吧,显得太low了,至于profile的变种trace吧,垂垂老矣,也一直没怎么用过. ...
- ueditor集成自己的ImageServer时出现错误的原因分析
1.场景:应用是一个独立的站点,ImageServer是一个独立的站点,因此存在跨域的问题. 2.遇到的详细错误“网络链接错误,请检查配置后重试!” 我使用uploadify测试是没问题的.使用ued ...
- 关闭浏览器后Session失效原因分析
参考文章:http://www.tuicool.com/articles/VNbYjqm 首先需要理解一下几点: 1.Http是无状态的,即对于每一次请求都是一个全新的请求,服务器不保存上一次请求的信 ...
- 解决VS2008之后平台(如VS2012/VS2013/VS2015)调试模式下不显示主界面窗口的问题
问题描述:win10操作系统下,VS2008工程调试模式下正常显示主界面窗口,使用VS2012/VS2013/VS2015环境打开VS2008工程,调试模式下应用程序转为后台进程,不显示主界面窗口:另 ...
- WPF--Dispatcher.BeginInvoke()方法使用不当导致UI界面卡死的原因分析
原文地址: http://www.tuicool.com/articles/F7reem http://blog.csdn.net/yl2isoft/article/details/11711833 ...
- Dispatcher.BeginInvoke()方法使用不当导致UI界面卡死的原因分析
原文:Dispatcher.BeginInvoke()方法使用不当导致UI界面卡死的原因分析 前段时间,公司同事开发了一个小工具,在工具执行过程中,UI界面一直处于卡死状态. 通过阅读代码发现,主要是 ...
- xtraTabbedMdiManager 双击最大化和关闭后返回主界面 z
双击tab头部时候子窗体Float时界面最大化和关闭Float状态的子窗体并不是真正关闭而是回到主界面的问题, 代码如下,其中xtraTabbedMdiManager1_Floating这个是xtra ...
随机推荐
- 《自拍教程24》在Windows上配置环境变量
我们说的环境变量,一般是指的是Path环境变量. 第一步:点击"我的电脑",右键,"属性" 第二步:点击"高级系统设置",弹出的窗口选&qu ...
- 常用传输层协议(tcp/ip+udp)与常用应用层协议简述(http)
一.计算机网络体系结构 二.TCP与UDP差异 1.TCP是面向连接的可靠传输,UDP是面向无连接的不可靠传输 面向连接表现在3次握手,4次挥手:可靠传输表现在未进行4次挥手时的差错重传,超时重传: ...
- 搞定 ElasticSearch系列一 下载安装
一.安装jdk 二.安装ElasticSearch 1.ElasticSearch下载地址: 2: 配置ElasticSearch 3:启动ElasticSearch 4: 安装ElasticSear ...
- Spring(七)核心容器 - 钩子接口
目录 前言 1.Aware 系列接口 2.InitializingBean 3.BeanPostProcessor 4.BeanFactoryPostProcessor 5.ImportSelecto ...
- MySQL 什么是索引?
该文为< MySQL 实战 45 讲>的学习笔记,感谢查看,如有错误,欢迎指正 一.索引简介 索引就类似书本的目录,作用就是方便我们更加快速的查找到想要的数据. 索引的实现方式比较多,常见 ...
- 宿主机休眠后,虚拟机网络ping不通网关
宿主机 win10 64位 虚拟机软件 vmware 15 虚拟机 centos 7 64位 网络模式:桥接模式 故障起因: 中午去吃饭,为了节省电费,把宿主机 windows 给休眠了 吃完饭 ...
- Kubernetes 部署 Nebula 图数据库集群
Kubernetes 是什么 Kubernetes 是一个开源的,用于管理云平台中多个主机上的容器化的应用,Kubernetes 的目标是让部署容器化的应用简单并且高效,Kubernetes 提供了应 ...
- CMD命令下图片合成一句话木马命令
非常简单,我们只需要一张图片1.jpg一句话木马写好的php文件 1.php之后我们进入到命令行.注意:将php文件和图片文件放到同一目录下,cmd也要跳转到放文件的目录下之后执行命令 copy .j ...
- Orleans[NET Core 3.1] 学习笔记(四)( 2 )获取Grain的方式
简介 在这一节,我们将介绍如何在Silo和Client中获取Grain及调用Grain Grain获取方式 从Grain内部获取: //根据特定的Key值创建或获取指定的Grain IStudent ...
- ihandy2019笔记编程真题
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...