在我的前两篇博客中,给大家介绍了在串口通信中创建子线程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方法就更加的健壮了。