在我的前两篇博客中,给大家介绍了在串口通信中创建子线程Read方法,来读取串口中的数据,以及如何控制我们Read方法读取的次数。但还有很重要的一点就是在这个过程中,Read方法是Send方法开启的一个新的线程,在这个处理过程中,如果出现了错误怎么办,很多人第一想到的是使用Try{}Catch{}方法来捕捉,但是我想说的是在.net 中,主线程是无法捕捉到子线程的错误的。首先看一段C#代码:运行后发现主线程通过try{}catch{}是不能扑捉子线程中的抛出来的异常:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Threading; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { try { System.Threading.Thread thread = new System.Threading.Thread(new Program().run); thread.Start(); } catch (Exception ex) { Console.WriteLine(ex.Message); } Thread.Sleep(1000); } public void run() { throw new Exception(); } } }
为什么呢?
首先需要了解异常的实现机制:异常的实现机制是严重依赖与线程的栈的。每个线程都有一个栈,线程启动后会在栈上安装一些异常处理帧,并形成一个链表的结构,在异常发生时通过该链表可以进行栈回滚,如果你自己没有安装的话,可能会直接跳到链表尾部,那可能是CRT提供的一个默认处理帧,弹出一个对话框提示某某地址内存错误等,然后确认调试,取消关闭。
所以说,线程之间是不可能发生异常处理的交换关系的。
但是在实际的程序设计中,会牵涉到扑捉子线程的异常,那么该怎样解决这个问题呢?
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Threading; namespace ConsoleApplication1 { class Program { private delegate void ExceptionHandler(Exception ex); private static ExceptionHandler exception; private static void ProcessException(Exception ex) { Console.WriteLine(ex.Message); Console.Read(); } static void Main(string[] args) { exception = new ExceptionHandler(ProcessException); System.Threading.Thread thread = new System.Threading.Thread(new Program().run); thread.Start(); Thread.Sleep(1000); } public void run() { try { throw new Exception("子线程异常"); } catch (Exception ex) { if (exception != null) { exception(ex); } } } } }
上面使用委托的方式,间接的解决了:把子线程中的异常信息交个主线程的一个方法去执行。(通过委托方式)
在我们的Read方法中,同样采用这样的处理方法,当然也不能忘了我们的目的是要把我们的东西全部封装起来,那怎么才能保证我们的错误在窗体中显示呢。我们我们throw出去的错误也没有人接收啊。所以我们完全可以发展一下这个方法,我们把捕捉到的错误交给我们原来写好的IDeal接口来处理,因为我们已经决定要想使用我们写好的协议来与串口进行通信,就必须实现IDeal方法这样我们才能知道我们返回的数据谁去处理,这两个完全是同样的道理,然后看我们的Read方法:
Private ideal As IDeal Private Sub Read() Try Dim str As String = "" ‘定义临时保存返回数据的字符串变量 Dim buf() As Byte ‘定义保存返回数据类型为byte的变量 Dim dataCount As Integer = _serialPort.BytesToRead ‘定义保存缓冲区数据的变量 Dim dataCountNew As Integer = 0 ‘定义保存最新的需要读取的缓冲区数据的变量 Dim CountTimes As Integer = 0 ‘尝试从缓冲区读数据的次数 ‘此处是本段的核心,1000*sleep(5)表示自己定义的最长等待串口返回数据的时间 ‘如果第一次取得的缓冲区的需要读取的数据为0,则线程暂停5ms(当然也可以自己定,数字越小性能未必会更好。合适就行)。一旦读到数据就执行下面的代码。或者循环完毕,也不能读到数据就会抛出异常。 While (dataCount = 0 And CountTimes < 1000) Thread.Sleep(5) ‘线程休眠5ms dataCount = _serialPort.BytesToRead ‘重新取出缓冲区待读到的数据 ‘如果取的的数据还为0 If (dataCount <= 0) Then ‘读取的次数加1 CountTimes = CountTimes + 1 Continue While ‘继续执行循环 Else ‘否则退出循环 Exit While End If End While ‘如果等待1000*5ms=5秒以后仍然没有读到数据 If dataCount = 0 Then ‘抛出异常 Throw New Exception("连接分机超时,请查看分机是否启动,或检查通讯网络!!") End If ‘如果读到数据,首先休眠500ms(这个数据一般是根据返回的数据的次数和晶振的频率,以及数据传输的一些时间设定,如果查询数据对于时间的性能要求不高可以适当的加长),再次读取缓冲区待读数据,和刚才读到的数据进行比较,如果相等,则说明读到的数据已经完整。可以进行处理。 While True Thread.Sleep(500) dataCountNew = _serialPort.BytesToRead ‘重新读取缓冲区数据的个数。 ‘如果不等则把这次读到的缓冲区数据的个数作为最新的数据保存 If dataCount <> dataCountNew Then dataCount = dataCountNew Else ‘如果相同,说明数据返回完全,退出循环 Exit While End If End While For i = 0 To dataCount ‘这时的循环,会根据实际情况的不同而不同。 Dim d As Integer d = _serialPort.ReadByte ‘从串口中读取一个字节的数据,如果设置了读取超时时间,在规定的时间未读到数据会触发超时错误。 str += Convert.ToString(d, 16).PadLeft(2, "0") Next buf = GetByte(str) ‘将收到的数据转换为字节数组。 ‘根据协议,处理收到的数据 Catch ex As Exception Me.DealEX(ex)‘当然这也可以用委托(但这样写也没有问题) End Try End Sub Public Sub DealEX(ByVal ex As Exception) ideal.DealEX(ex) End Sub这样我们的Read方法就更加的健壮了。