C# 串口操作系列(2) -- 入门篇,为什么我的串口程序在关闭串口时候会死锁

 

C# 串口操作系列(2) -- 入门篇,为什么我的串口程序在关闭串口时候会死锁 ?

标签: c#objectuibyte通讯.net
  55211人阅读  评论(188)  收藏  举报
  分类:
 

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

 

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

   

[c-sharp]  view plain  copy
  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。

 

[c-sharp]  view plain  copy
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.ComponentModel;  
  4. using System.Data;  
  5. using System.Drawing;  
  6. using System.Linq;  
  7. using System.Text;  
  8. using System.Windows.Forms;  
  9. using System.IO.Ports;  
  10. using System.Text.RegularExpressions;  
  11. namespace SerialportSample  
  12. {  
  13.     public partial class SerialportSampleForm : Form  
  14.     {  
  15.         private SerialPort comm = new SerialPort();  
  16.         private StringBuilder builder = new StringBuilder();//避免在事件处理方法中反复的创建,定义到外面。  
  17.         private long received_count = 0;//接收计数  
  18.         private long send_count = 0;//发送计数  
  19.         private bool Listening = false;//是否没有执行完invoke相关操作  
  20.         private bool Closing = false;//是否正在关闭串口,执行Application.DoEvents,并阻止再次invoke  
  21.         public SerialportSampleForm()  
  22.         {  
  23.             InitializeComponent();  
  24.         }  
  25.         //窗体初始化  
  26.         private void Form1_Load(object sender, EventArgs e)  
  27.         {  
  28.             //初始化下拉串口名称列表框  
  29.             string[] ports = SerialPort.GetPortNames();  
  30.             Array.Sort(ports);  
  31.             comboPortName.Items.AddRange(ports);  
  32.             comboPortName.SelectedIndex = comboPortName.Items.Count > 0 ? 0 : -1;  
  33.             comboBaudrate.SelectedIndex = comboBaudrate.Items.IndexOf("9600");  
  34.             //初始化SerialPort对象  
  35.             comm.NewLine = "/r/n";  
  36.             comm.RtsEnable = true;//根据实际情况吧。  
  37.             //添加事件注册  
  38.             comm.DataReceived += comm_DataReceived;  
  39.         }  
  40.         void comm_DataReceived(object sender, SerialDataReceivedEventArgs e)  
  41.         {  
  42.             if (Closing) return;//如果正在关闭,忽略操作,直接返回,尽快的完成串口监听线程的一次循环  
  43.             try  
  44.             {  
  45.                 Listening = true;//设置标记,说明我已经开始处理数据,一会儿要使用系统UI的。  
  46.                 int n = comm.BytesToRead;//先记录下来,避免某种原因,人为的原因,操作几次之间时间长,缓存不一致  
  47.                 byte[] buf = new byte[n];//声明一个临时数组存储当前来的串口数据  
  48.                 received_count += n;//增加接收计数  
  49.                 comm.Read(buf, 0, n);//读取缓冲数据  
  50.                 builder.Clear();//清除字符串构造器的内容  
  51.                 //因为要访问ui资源,所以需要使用invoke方式同步ui。  
  52.                 this.Invoke((EventHandler)(delegate  
  53.                 {  
  54.                     //判断是否是显示为16禁止  
  55.                     if (checkBoxHexView.Checked)  
  56.                     {  
  57.                         //依次的拼接出16进制字符串  
  58.                         foreach (byte b in buf)  
  59.                         {  
  60.                             builder.Append(b.ToString("X2") + " ");  
  61.                         }  
  62.                     }  
  63.                     else  
  64.                     {  
  65.                         //直接按ASCII规则转换成字符串  
  66.                         builder.Append(Encoding.ASCII.GetString(buf));  
  67.                     }  
  68.                     //追加的形式添加到文本框末端,并滚动到最后。  
  69.                     this.txGet.AppendText(builder.ToString());  
  70.                     //修改接收计数  
  71.                     labelGetCount.Text = "Get:" + received_count.ToString();  
  72.                 }));  
  73.             }  
  74.             finally  
  75.             {  
  76.                 Listening = false;//我用完了,ui可以关闭串口了。  
  77.             }  
  78.         }  
  79.         private void buttonOpenClose_Click(object sender, EventArgs e)  
  80.         {  
  81.             //根据当前串口对象,来判断操作  
  82.             if (comm.IsOpen)  
  83.             {  
  84.                 Closing = true;  
  85.                 while (Listening) Application.DoEvents();  
  86.                 //打开时点击,则关闭串口  
  87.                 comm.Close();  
  88.                 Closing = false;  
  89.             }  
  90.             else  
  91.             {  
  92.                 //关闭时点击,则设置好端口,波特率后打开  
  93.                 comm.PortName = comboPortName.Text;  
  94.                 comm.BaudRate = int.Parse(comboBaudrate.Text);  
  95.                 try  
  96.                 {  
  97.                     comm.Open();  
  98.                 }  
  99.                 catch(Exception ex)  
  100.                 {  
  101.                     //捕获到异常信息,创建一个新的comm对象,之前的不能用了。  
  102.                     comm = new SerialPort();  
  103.                     //现实异常信息给客户。  
  104.                     MessageBox.Show(ex.Message);  
  105.                 }  
  106.             }  
  107.             //设置按钮的状态  
  108.             buttonOpenClose.Text = comm.IsOpen ? "Close" : "Open";  
  109.             buttonSend.Enabled = comm.IsOpen;  
  110.         }  
  111.         //动态的修改获取文本框是否支持自动换行。  
  112.         private void checkBoxNewlineGet_CheckedChanged(object sender, EventArgs e)  
  113.         {  
  114.             txGet.WordWrap = checkBoxNewlineGet.Checked;  
  115.         }  
  116.         private void buttonSend_Click(object sender, EventArgs e)  
  117.         {  
  118.             //定义一个变量,记录发送了几个字节  
  119.             int n = 0;  
  120.             //16进制发送  
  121.             if (checkBoxHexSend.Checked)  
  122.             {  
  123.                 //我们不管规则了。如果写错了一些,我们允许的,只用正则得到有效的十六进制数  
  124.                 MatchCollection mc = Regex.Matches(txSend.Text, @"(?i)[/da-f]{2}");  
  125.                 List<byte> buf = new List<byte>();//填充到这个临时列表中  
  126.                 //依次添加到列表中  
  127.                 foreach (Match m in mc)  
  128.                 {  
  129.                     buf.Add(byte.Parse(m.Value));  
  130.                 }  
  131.                 //转换列表为数组后发送  
  132.                 comm.Write(buf.ToArray(), 0, buf.Count);  
  133.                 //记录发送的字节数  
  134.                 n = buf.Count;  
  135.             }  
  136.             else//ascii编码直接发送  
  137.             {  
  138.                 //包含换行符  
  139.                 if (checkBoxNewlineSend.Checked)  
  140.                 {  
  141.                     comm.WriteLine(txSend.Text);  
  142.                     n = txSend.Text.Length + 2;  
  143.                 }  
  144.                 else//不包含换行符  
  145.                 {  
  146.                     comm.Write(txSend.Text);  
  147.                     n = txSend.Text.Length;  
  148.                 }  
  149.             }  
  150.             send_count += n;//累加发送字节数  
  151.             labelSendCount.Text = "Send:" + send_count.ToString();//更新界面  
  152.         }  
  153.         private void buttonReset_Click(object sender, EventArgs e)  
  154.         {  
  155.             //复位接受和发送的字节数计数器并更新界面。  
  156.             send_count = received_count = 0;  
  157.             labelGetCount.Text = "Get:0";  
  158.             labelSendCount.Text = "Send:0";  
  159.         }  
  160.     }  
  161. }  

 

至此,不会再出现关闭死锁问题了。

 

希望这篇文章能解你的燃眉之急,非常高兴能与读者分享我层遇到,大多数人都遇到的这个问题。如果说的不明白,欢迎讨论。

后续的有关通讯程序底层设计的文章会讲述一个具有丰富扩展性,但有设计简介的万能通讯库,支持网络、蓝牙、串口通讯、并口通讯。但不要指望我都实现出来了,我只是设计出这个框架。

 

示例代码

 

//append by wuyazhe @2011-5-26

上面有一点疏漏,源自第一篇,结果到这里还是没修改,源码中有一行,需要修改一下。

//发送按钮中

buf.Add(byte.Parse(m.Value));

要修改为

buf.Add(byte.Parse(m.Value,System.Globalization.NumberStyles.HexNumber));

已标记关键词 清除标记
相关推荐
DirectX修复工具(DirectX Repair)是一款系统级工具软件,简便易用。本程序为绿色版,无需安装,可直接运行。 本程序的主要功能是检测当前系统的DirectX状态,如果发现异常则进行修复。程序主要针对0xc000007b问题设计,可以完美修复该问题。本程序中包含了最新版的DirectX redist(Jun2010),并且全部DX文件都有Microsoft的数字签名,安全放心。 本程序为了应对一般电脑用户的使用,采用了易用的一键式设计,只要点击主界面上的“检测并修复”按钮,程序就会自动完成校验、检测、下载、修复以及注册的全部功能,无需用户的介入,大大降低了使用难度。在常规修复过程中,程序还会自动检测DirectX加速状态,在异常时给予用户相应提示。 本程序适用于多个操作系统,如Windows XP(需先安装.NET 2.0,详情请参阅“致Windows XP用户.txt”文件)、Windows Vista、Windows 7、Windows 8、Windows 8.1、Windows 8.1 Update、Windows 10,同时兼容32操作系统和64位操作系统。本程序会根据系统的不同,自动调整任务模式,无需用户进行设置。 本程序的V4.0版分为标准版、增强版以及在线修复版。所有版本都支持修复DirectX的功能,而增强版则额外支持修复c++的功能。在线修复版功能与标准版相同,但其所需的数据包需要在修复时自动下载。各个版本之间,主程序完全相同,只是其配套使用的数据包不同。因此,标准版和在线修复版可以通过补全扩展包的形式成为增强版。本程序自V3.5版起,自带扩展功能。只要在主界面的“工具”菜单下打开“选项”对话框,找到“扩展”标签,点击其中的“开始扩展”按钮即可。扩展过程需要Internet连接,扩展成功后新的数据包可自动生效。扩展用时根据网络速度不同而不同,最快仅需数秒,最慢需要数分钟,烦请耐心等待。如扩展失败,可点击“扩展”界面左上角小锁图标切换为加密连接,即可很大程度上避免因防火墙或其他原因导致的连接失败。 本程序自V2.0版起采用全新的底层程序架构,使用了异步多线程编程技术,使得检测、下载、修复单独进行,互不干扰,快速如飞。新程序更改了自我校验方式,因此使用新版本的程序时不会再出现自我校验失败的错误;但并非取消自我校验,因此程序安全性与之前版本相同,并未降低。 程序有更新系统c++功能。由于绝大多数软件运行时需要c++的支持,并且c++的异常也会导致0xc000007b错误,因此程序在检测修复的同时,也会根据需要更新系统中的c++组件。自V3.2版本开始使用了全新的c++扩展包,可以大幅提高工业软件修复成功的概率。修复c++的功能仅限于增强版,标准版及在线修复版在系统c++异常时(非丢失时)会提示用户使用增强版进行修复。除常规修复外,新版程序还支持C++强力修复功能。当常规修复无效时,可以到本程序的选项界面内开启强力修复功能,可大幅提高修复成功率。请注意,请仅在常规修复无效时再使用此功能。 程序有两种窗口样式。正常模式即默认样式,适合绝大多数用户使用。另有一种简约模式,此时窗口将只显示最基本的内容,修复会自动进行,修复完成10秒钟后会自动退出。该窗口样式可以使修复工作变得更加简单快速,同时方便其他软件、游戏将本程序内嵌,即可进行无需人工参与的快速修复。开启简约模式的方法是:打开程序所在目录下的“Settings.ini”文件(如果没有可以自己创建),将其中的“FormStyle”一项的值改为“Simple”并保存即可。 新版程序支持命令行运行模式。在命令行中调用本程序,可以在路径后直接添加命令进行相应的设置。常见的命令有7类,分别是设置语言的命令、设置窗口模式的命令,设置安全级别的命令、开启强力修复的命令、设置c++修复模式的命令、控制Direct加速的命令、显示版权信息的命令。具体命令名称可以通过“/help”或“/?”进行查询。 程序有高级筛选功能,开启该功能后用户可以自主选择要修复的文件,避免了其他不必要的修复工作。同时,也支持通过文件进行辅助筛选,只要在程序目录下建立“Filter.dat”文件,其中的每一行写一个需要修复文件的序号即可。该功能仅针对高级用户使用,并且必须在正常窗口模式下才有效(简约模式时无效)。 本程序有自动记录日志功能,可以记录每一次检测修复结果,方便在出现问题时,及时分析和查找原因,以便找到解决办法。 程序的“选项”对话框中包含了7项高级功能。点击"常规”选项卡可以调整程序的基本运行情况,包括日志记录、安全级别控制、调试模式开启等。只有开启调试模式后才能在C
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页