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

原文: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源码,发现这种解决问题的方法还是很有用处的。结果不重要,解决问题的方法是最重要的。

原文地址:https://www.cnblogs.com/lonelyxmas/p/12105162.html

时间: 2024-07-29 11:13:05

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

winform 防止主界面卡死

总结网络上的解决方案:新线程=> 委托=> 主界面的异步更新方法(IAsyncResult BeginInvoke(Delegate method)),一句话就是通过委托调用另一个线程的异步方法. 验证方法:当线程执行时,拖拽主窗体,没有卡死迹象. 1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawi

MySQL在删除表时I/O错误原因分析

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由腾讯数据库技术 发表于云+社区专栏 问题现象 最近使用sysbench测试MySQL,由于测试时间较长,写了一个脚本按prepare->run->cleanup的顺序在后台跑着.跑完后察看日志发现一个问题,MySQL服务的错误日志中出现多条类似以下信息的报错: [ERROR] InnoDB: Trying to do I/O to a tablespace which does not exist. I/O type: read

SQL Server扩展事件的使用ring_buffer target时“丢失”事件的原因分析以及ring_buffer target潜在的问题

事情起因: 排查SQL Server上的死锁问题,一开始想到的就是扩展事件, 第一种方案,开profile守株待兔吧,显得太low了,至于profile的变种trace吧,垂垂老矣,也一直没怎么用过. 第二种方案是开启TRACEON(DBCC TRACEON (3605,1204,1222,-1))将死锁写入error log,也是个不错的选择. 不过想到系统默认的扩展事件sysem_health已经捕获了死锁信息(sqlserver.xml_deadlock_report), 就没必要再重新往

解决异步回调方法中,初始化Cef时WPF界面卡死的问题

Application.Current.Dispatcher.BeginInvoke(new Action(() =>                            { })); 注意用Invoke同样会卡死,只能用BeginInvoke

ueditor集成自己的ImageServer时出现错误的原因分析

1.场景:应用是一个独立的站点,ImageServer是一个独立的站点,因此存在跨域的问题. 2.遇到的详细错误“网络链接错误,请检查配置后重试!” 我使用uploadify测试是没问题的.使用ueditor的upload对话框上传时出现了这个问题,因此我怀疑是跨域文件写法有我问题. 我原来的写法: 经google发现,flashplayer升级到9.124之后,加强了安全性,之前的crossdomain.xml的写法发生了变化,以下就是该文件的新版写法: 加上allow-http-reques

关闭浏览器后Session失效原因分析

参考文章:http://www.tuicool.com/articles/VNbYjqm 首先需要理解一下几点: 1.Http是无状态的,即对于每一次请求都是一个全新的请求,服务器不保存上一次请求的信息 2.Session是保存在服务端的,为什么后续请求会读取到session?因为请求会包含一个sessionId,该值存储在cookie中,服务器通过这个sessionId找到对应的session 3.cookie是有过期时间的,规则如下: Cookie的Max-Age决定了Cookie的有效期,

C# Serialport执行close()方法时,程序卡死的解决办法

根据网上搜到的文章,程序中添加两个bool变量,作为状态标记,保证串口关闭时,串口事件已处理完 private volatile bool is_serial_listening = false;//串口正在监听标记private volatile bool is_serial_closing = false;//串口正在关闭标记 //Program Begins using System;using System.Collections.Generic;using System.Compone

Dispatcher.BeginInvoke()方法使用不当导致UI界面卡死的原因分析

原文:Dispatcher.BeginInvoke()方法使用不当导致UI界面卡死的原因分析 前段时间,公司同事开发了一个小工具,在工具执行过程中,UI界面一直处于卡死状态. 通过阅读代码发现,主要是由于Dispatcher.BeginInvoke()方法使用不当导致的. 本文将通过一个WPF模拟程序来演示一下界面卡死的现象,并通过修改代码来解决界面卡死的问题. 希望通过对本文的学习,大家能对Dispatcher.BeginInvoke()方法有一个新的认识. 文章开篇直接给出界面卡死的示例代码

Windows Server 2016 - 关闭开机显示的管理服务页和关机时必须要输入原因

虽然服务器不需要经常开关机,但是毕竟是我家用,有时候甚至是把它当成一个极简的Windows10系统.每回开机就立刻跳出管理服务器的界面,而且加载还及其缓慢.让我想起了开机广告的一刀传奇.关机的时候,必须要输入原因才能进行,也是很麻烦.有时候就是想关机,并没有太多的理由.还好这两项都在一个地方可以关闭. 关闭这个讨厌的界面,Win + R, 打开gpedit.msc 按如下路径找到系统选项,关闭这两个功能都在一个地方. 启用“ 不显示‘管理你的服务器’ ”. 禁用“ 显示‘关闭时间跟踪程序’ ”.