上文中讲述了工作者线程将数据同步到GUI中的第一种方式,本文讲述第二种方式:
Control.Invoke()与Control.BeginInvoke()
// 摘要:
// 在拥有此控件的基础窗口句柄的线程上执行指定的委托。
//
// 参数:
// method:
// 包含要在控件的线程上下文中调用的方法的委托。
//
// 返回结果:
// 正在被调用的委托的返回值,或者如果委托没有返回值,则为 null。
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public object Invoke(Delegate method);
//
// 摘要:
// 在拥有控件的基础窗口句柄的线程上,用指定的参数列表执行指定委托。
//
// 参数:
// method:
// 一个方法委托,它采用的参数的数量和类型与 args 参数中所包含的相同。
//
// args:
// 作为指定方法的参数传递的对象数组。如果此方法没有参数,该参数可以是 null。
//
// 返回结果:
// System.Object,它包含正被调用的委托返回值;如果该委托没有返回值,则为 null。
public object Invoke(Delegate method, params object[] args);
//
// 摘要:
// 在创建控件的基础句柄所在线程上异步执行指定委托。
//
// 参数:
// method:
// 对不带参数的方法的委托。
//
// 返回结果:
// 一个表示 System.Windows.Forms.Control.BeginInvoke(System.Delegate) 操作的结果的 System.IAsyncResult。
//
// 异常:
// System.InvalidOperationException:
// 找不到适当的窗口句柄。
[EditorBrowsable(EditorBrowsableState.Advanced)]
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public IAsyncResult BeginInvoke(Delegate method);
//
// 摘要:
// 在创建控件的基础句柄所在线程上,用指定的参数异步执行指定委托。
//
// 参数:
// method:
// 一个方法委托,它采用的参数的数量和类型与 args 参数中所包含的相同。
//
// args:
// 作为给定方法的参数传递的对象数组。如果不需要参数,则可以为 null。
//
// 返回结果:
// 一个表示 System.Windows.Forms.Control.BeginInvoke(System.Delegate) 操作的结果的 System.IAsyncResult。
//
// 异常:
// System.InvalidOperationException:
// 找不到适当的窗口句柄。
[EditorBrowsable(EditorBrowsableState.Advanced)]
public IAsyncResult BeginInvoke(Delegate method, params object[] args);
其中注意BeginInvoke与I/O调用中的BeginXxx的参数不一致。
BeginXxx有参数AsyncCallback作为回调函数,参数state是回调函数的实参。可以通过查询回调方法类型IAsyncResult中的只读AsyncState属性访问自己的state。CLR内部容纳了一个IAsyncResult对象引用,当BeginXxx完成后,一个线程池线程会调用回调方法,并传递内部维护的IAsyncResult的引用。因此在回调方法中可以方便的调用EndXxx。
BeginInvoke调用的回调函数不是AsyncCallback型委托因此,除非把BeginInvoke的返回值作为参数传入到回调方法中,否则很难调用EndInvoke。(不过似乎Control.EndInvoke不用调用也可以正确执行,这点还要进一步证实,目前没有结论。)
跟上节C#多线程值之APM二:GUI线程处理模式1代码基本一直,将改动代码复制如下:
private void SerialPortCallBack(IAsyncResult result)
{
try
{
int count = serialPort1.BaseStream.EndRead(result);
byte[] data = (byte[])result.AsyncState;
//Console.WriteLine(result.AsyncWaitHandle.GetHashCode());
Console.WriteLine("\nCallBack ID: " + System.Threading.Thread.CurrentThread.GetHashCode());
for (int i = 0; i < count; i++)
{
Console.Write("{0:X2} ", data[i]);
if (this.richTextBox1.InvokeRequired)
{
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText(string.Format("{0:X2} ", data[i]));
Console.WriteLine("RichTextBox Invoke ID:" + System.Threading.Thread.CurrentThread.GetHashCode());
}));
}
else
{
richTextBox1.AppendText(string.Format("{0:X2} ", data[i]));
}
}
}
catch (IOException ex)
{
MessageBox.Show(ex.Message);
}
}
这里调用的是Invoke,发送端界面与上节一致,这里仅展示接收端结果界面中的控制台应用程序。
从结果中可以看出:
事件线程会变化,串口异步回调函数线程也会变化,但是RichTextBox.Invoke始终调用主界面线程。界面可快速响应鼠标操作。同时注意到与66基本在一块的显示却跑到了BB与CC之间,这明显的是由于回调线程的异步性导致的(一个是11,一个是13)两者争夺控制面板显示
我们在做一个实验,让RichTextBox.Invoke中调用Thread.Sleep(1000)
if (this.richTextBox1.InvokeRequired)
{
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText(string.Format("{0:X2} ", data[i]));
Console.WriteLine("RichTextBox Invoke ID:" + System.Threading.Thread.CurrentThread.GetHashCode());
Thread.Sleep(1000);
}));
}
else。。。
此时GUI界面完全不响应,从事也堵塞了回调函数,因此回调线程ID从9一直上涨至423,非常浪费资源。
这一现象也发生在上一节中的Post中。因此为了提高用户体验,尽可能降低GUI线程的工作量。
至于BeginInvoke的调用放在下一节对比讲述。