.NET中,许多IO设备的事件是在辅助线程上引发,如System.IO.Ports.SerialPort的DataRecieved事件。在MSDN中讲道:
从 SerialPort 对象接收数据时,将在辅助线程上引发 DataReceived 事件。 由于此事件在辅助线程而非主线程上引发,因此尝试修改主线程中的一些元素(如 UI 元素)时会引发线程异常。如果有必要修改主 Form 或 Control 中的元素,请使用 Invoke 回发更改请求,这将在正确的线程上执行。
在实际使用中,若需要同步调用,则使用Control.Invoke方法,异步调用使用Control.BeginInvoke方法(WPF中使用Dispatcher.Invoke和Dispatcher.BeginInvoke)。这几个方法都由两个重载,声明如下:
名称 | 说明 |
在拥有此控件的基础窗口句柄的线程上执行指定的委托。 | |
Invoke(Delegate, Object [] ) | 在拥有控件的基础窗口句柄的线程上,用指定的参数列表执行指定委托。 |
例如,在一个串口DataReceived接收事件中,当接收到一个指定字串后,需要在Form上某个TextBox中显示接收到的数据,则可以这样使用:
public delegate void HandleReceivedCallback(string text); //…… private void HandleReceived(string text) { this.textBox1.Text = text; }
private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { string text = serialPort1.ReadLine(); this.Invoke(new HandleReceivedCallback (HandleReceived), new object[] { text }); } |
即要跨线程调用,应该包含3个步骤:
1) 声明一个调用委托;
2) 定义一个调用函数(委托实例);
3) 采用Invoke调用该委托。
若存在多处跨线程调用,而每一个被调用函数都比较简单且仅在此处调用,则使用上面的方法会增加较多重复的操作(声明委托、定义函数),此时可以利用匿名委托和匿名函数来简化该步骤。
在.NET 3.5以后,System命名空间提供了两类委托泛型:Action和Func。其中,Action用来代表无返回值委托,Func用来代表有返回值委托。同时,.NET 3.5也提供了匿名方法(lambda表达式实际上也是一种匿名方法)来作为内联编码方式。上面的操作如果采用匿名委托和匿名方法,则可以这样使用:
private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { string text = serialPort1.ReadLine(); this.Invoke(new Action<string>(delegate(string txt) { this.textBox1.Text = txt; }), new object[] { text }); } |
这段代码与之前的代码作用完全相同,但其减少了声明委托、定义方法的操作。对于有较多只在一处线程间调用的,这种实现方法可以减少相当一部分工作量,并且可以使程序代码更加清晰。
注:在辅助线程上引发的常用对象还有System.Timers.Timer类的Elapsed事件。不过Timer类提供了用于操作用户界面提供了另一种简便的方法:如果和用户界面元素(如窗体或控件)一起使用 Timer,请将包含有 Timer 的窗体或控件赋值给 SynchronizingObject 属性,以便将此事件封送到用户界面线程中。