DotNet · 2013年8月16日

为什么我的串口程序在关闭串口时候会死锁 ?

我的简单总结:

SerialPort类接收到数据后,如果通过主线程来更新界面,那么,串口类的锁会处于等待界面,等待这个处理过程的结束,此时如果主线程有事件处于中断状态,比如点击事件,而此事件最终调用了SerialPort的关闭事件,则关闭事件需要串口内部的锁来关闭,此时就会产生死锁,导致无法关闭

解决的方法,就是在接收数据事件中,不要直接使用主线程来传递数据,应该使用BeginInvoke等方法来避免死锁

 

下面是网上参考的原文


第一篇文章我相信很多人不看都能做的出来,但是,用过微软SerialPort类的人,都遇到过这个尴尬,关闭串口的时候会让软件死锁。天哪,我可不是武断,算了。不要太绝对了。99.9%的人吧,都遇到过这个问题。我想只有一半的人真的解决了。另外一半的人就睁只眼闭只眼阿弥佗佛希望不要在客户那里出现这问题了。

 

   
你看到我的文章,就放心吧,这问题有救了。我们先回顾一下上一篇中的代码

   

[c-sharp] view plaincopy

  1. void comm_DataReceived(object sender, SerialDataReceivedEventArgs e)     
  2. {     
  3.     //先记录下来,避免某种原因,人为的原因,操作几次之间时间长,缓存不一致  
  4.     int n = comm.BytesToRead;  
  5.     //声明一个临时数组存储当前来的串口数据  
  6.     byte[] buf = new byte[n];     
  7.     //增加接收计数  
  8.     received_count += n;   
  9.     //读取缓冲数据    
  10.     comm.Read(buf, 0, n);     
  11.     //清除字符串构造器的内容  
  12.     builder.Clear();     
  13.     //因为要访问ui资源,所以需要使用invoke方式同步ui。     
  14.     this.Invoke((EventHandler)(delegate{…界面更新,略}));     
  15. }     
  16.     
  17. private void buttonOpenClose_Click(object sender, EventArgs e)     
  18. {     
  19.     //根据当前串口对象,来判断操作     
  20.     if (comm.IsOpen)     
  21.     {     
  22.         //打开时点击,则关闭串口     
  23.         comm.Close();//这里就是可能导致软件死掉的地方  
  24.     }     
  25.     else    
  26.     {…}    
  27. }  

 

   
为什么会死锁呢,并发冲突。

   
我们要了解一下SerialPort的实现和串口通讯机制,在你打开串口的时候,SerialPort会创建一个监听线程ListenThread,在这个线程中,等待注册的串口中断,当收到中断后,会调用DataReceived事件。调用完成后,继续进入循环等待,直到串口被关闭退出线程。

   
我们的UI主线程如何做的呢,首先创建一个窗体,然后执行了Application.Run(窗体实例)。是这样把,这里的Application.Run就是创建了一个消息循环,循环的处理相关的消息。

   
这里我们就有了2个线程,UI主线程、串口监听线程。那么你在DataReceived处理数据的时候,就需要线程同步,避免并发冲突,什么是并发冲突?并发冲突就是2个或多个并行(至少看上去像)的线程运行的时候,多个线程共同的操作某一线程的资源,在时序上同时或没有按我们的预计顺序操作,这样就可能导致数据混乱无序或是彼此等待完成死锁软件。

   
而串口程序大多是后者。为什么呢,看看我们的例子中DataReceived做了什么?首先读取数据,然后就是调用this.Invoke方法更新UI了。这里Invoke的时候,监听线程将等待UI线程的标志,等到后,开始操作UI的资源,当操作完成之前,监听线程也就停在DataReceived方法的调用这里,如果这个时候。并发了关闭串口的操作会如何呢?SerialPort的Close方法,会首先尝试等待和监听线程一样的一个互斥体、临界区、或是事件(不确定.net用的哪种)。那这个同步对象什么时候释放呢?每次循环结束就释放,哦。循环为什么不结束呢?因为这一次的循环操作执行到DataReceived之后,执行了Invoke去更新界面了,那Invoke怎么又没有执行完成呢?看上去很简单的几行代码。虽然我没仔细研读过.net的Invoke原理,但我猜测是通过消息的方式来同步的,这也是为什么这么多的类,只有控件(窗体也是控件的一种,.net在概念上,颠覆了微软自己的概念,传统的win32编程,是说所有的控件都是个window,只是父窗体不同,表现形式不同,但都是基于系统消息队列的,.net出于更高的抽象,正好反过来了。呵呵)才有Invoke方法了。(委托自己的Invoke和这个不同)

   
我猜测控件/窗体的Invoke是SendMessage方式实现的,那么发送消息后就会等待消息循环来处理消息了。如果你直接去关闭串口了。你点击按钮本身也会被转换成消息WM_CLICK,消息循环在处理按钮的WM_CLICK时候,调用你按钮的OnClick方法,进而触发调用你的ButtonClose_Click事件,这都是同步调用的,你的主线程,处理消息的过程,停在了这个Click事件,而你的Click事件又去调用了SerialPort的Close方法,Close方法又因为和串口监听线程的同步信号量关联在一起需要等待一次的while结束,而这个while循环中调用了DataReceived方法,这个方法中调用了Invoke,也就是发送了消息到消息队列等待结果,但消息循环正在处理你的关闭按钮事件等待退出。

 

   
实在太复杂了,这个情况下,你想要真的关闭串口成功,就需要while中的DataReceived方法调用结束释放同步信号,就需要执行完Invoke,就需要执行消息循环,幸运的是,我们真的有办法执行消息循环来打破僵局。Application.DoEvents()。还好,不幸中的万幸。可是问题又来了,你能让Invoke结束,但你无法确定是否在你调用消息循环后,你的某一时刻不会再次并发,可能由于单cpu的串行操作模拟并行中,又把时间片先分给了优先级高的串口监听线程呢?是有可能的。所以,我们就需要一点方法来避免再次invoke窗体。优化后不会司机的例子如下,我们修改DataReceived方法,关闭方法,并定义2个标记Listening和Closing。

 

最新电影,电视剧,尽在午夜剧场

电影电视剧午夜不寂寞