To invoke and to begin invoke, that is a question.
To invoke and to begin invoke, that is a question.
在多线程的应用程序中经常会涉及到操作System.Windows.Forms.Control,我们经常会遇到一些常见的问题例如“为什么UI界面被挂起了,失去响应了”,等等。其实Control类已经提供了一套简单的机制来帮助我们处理这些问题,本文将会重点阐述该机制,对于一些常见问题进行解释。
主要内容:
- 基础概念。
- 调用Invoke和BeginInvoke时,指定的Delegate会在那个线程运行?如何决定使用Invoke还是BeginInvoke?
- BeginInvoke和EndInvoke必须成对使用吗?
- 实现细节。
基础概念
.Net framework 定义了一个接口ISynchronizeInvoke来处理方法的同步及异步调用。
2 {
3 // Methods
4 IAsyncResult BeginInvoke(Delegate method, object[] args);
5 object EndInvoke(IAsyncResult result);
6 object Invoke(Delegate method, object[] args);
7
8 // Properties
9 bool InvokeRequired { get; }
10 }
InvokeRequired:对于实现该接口的类型而言,当客户端尝试操作该类型的时候InvokeRequired将会要求客户端使用Invoke或者BeginInvoke方法来执行操作。
而Invoke和BeginInvoke则分别以同步和异步的方式来进行操作。
System.Windows.Forms.Control就实现了这个接口。
调用Invoke和BeginInvoke时,指定的Delegate会在那个线程运行?如何决定使用Invoke还是BeginInvoke?
在调用Invoke和BeginInvoke的时候,客户端必须提供一个指定的Delegate,那么这个Delegate是在那个线程执行那?
MSDN的定义:
- Invoke, Executes the specified delegate on the thread that owns the control's underlying window handle.
- BeginInvoke,Executes the specified delegate asynchronously on the thread that the control's underlying handle was created on.
从定义上我们可以看到Invoke和BeginInvoke都会在拥有控件Handle的线程上执行,也就是创建该控件的UI线程。
看个例子:
构建一个新的Windows Form, 添加一个Button控件到Form上,添加如下代码:
2 {
3 private delegate void OutputMessage(string content);
4 private IAsyncResult result = null;
5 private System.Threading.Thread thread;
6
7 public Form1()
8 {
9 InitializeComponent();
10 }
11
12 private void button1_Click(object sender, EventArgs e)
13 {
14 Console.WriteLine(string.Format("UI Thread Id : {0}", System.Threading.Thread.CurrentThread.ManagedThreadId));
15
16 thread = new System.Threading.Thread(new System.Threading.ThreadStart(this.Test));
17 thread.Start();
18 }
19
20 private void Test()
21 {
22 Console.WriteLine(string.Format("New Thread Id : {0}", System.Threading.Thread.CurrentThread.ManagedThreadId));
23 Console.WriteLine("Start executing...");
24 this.Invoke(new OutputMessage(this.WriteMessage), "Hello World!");
25 //result = this.BeginInvoke(new OutputMessage(this.WriteMessage), "hello wrold!");
26
27 Console.WriteLine("Finished executing...");
28 }
29
30 private void WriteMessage(string content)
31 {
32 Console.WriteLine(string.Format("Invoke Thread Id : {0}", System.Threading.Thread.CurrentThread.ManagedThreadId));
33 Console.WriteLine(content);
34 }
35 }
点击Button控件,Console输出如下:
UI Thread Id : 9
New Thread Id : 10
Start executing...
Invoke Thread Id : 9
Hello World!
Finished executing...
从上面的输出可以看出(蓝色部分在主线程,红色部分在新开启线程),尽管调用者是在一个新启动的线程中,但是Invoke中所指定Delegate是在主UI线程中执行的,而且是同步的,也就是说:只有“Hello World!”被输出后,新启动线程才会继续执行。
将第24行代码注释掉,同时打开第25行代码,进行BeginInvoke调用,输出如下:
UI Thread Id : 9
New Thread Id : 10
Start executing...
Finished executing...
Invoke Thread Id : 9
Hello World!
从上面的输出可以看出(蓝色部分在主线程,红色部分在新开启线程),在进行BeginInvoke调用的时候,指定的Delegate依然在主UI线程中运行。但是BeginInvoke并不会阻塞新开启的调用者线程,调用者线程将BeginInvoke交给执行线程后,会继续执行。所以在上面的例子中“Finished executing...”会先于“Hello World!”输出。
以上就是Invoke与BeginInvoke的相同点与不同点,一个是同步执行,一个是异步执行,一个会堵塞调用者线程,一个不会。但都是在UI主线程(创建该控件的线程)上进行。
BeginInvoke和EndInvoke必须成对使用吗?
不需要, Control的BeginInvoke方法不需要在每次调用后,再调用相应的EndInvoke方法。EndInvoke方法的意图是获取异步执行方法的返回值,如果需要使用返回值,或者被ref、out标识的参数,则需要被调用。
再看一下上面的例子,更改Test以及WriteMessage方法:
2 {
3 Console.WriteLine(string.Format("New Thread Id : {0}", System.Threading.Thread.CurrentThread.ManagedThreadId));
4 Console.WriteLine("Start executing...");
5
6 result = this.BeginInvoke(new OutputMessage(this.WriteMessage), "Hello World!");
7 Console.WriteLine("Finished executing...");
8
9 Console.WriteLine("Start end invoke...");
10 object retValue = this.EndInvoke(this.result);
11 Console.WriteLine("Finished end invoke...");
12 }
13
14 private void WriteMessage(string content)
15 {
16 System.Threading.Thread.Sleep(5000);
17 Console.WriteLine(string.Format("Invoke Thread Id : {0}", System.Threading.Thread.CurrentThread.ManagedThreadId));
18 Console.WriteLine(content);
19 }
输出如下:
UI Thread Id : 9
New Thread Id : 10
Start executing...
Finished executing...
Start end invoke...
Invoke Thread Id : 9
Hello World!
Finished end invoke...
在WriteMessage中,我们让当前线程Sleep 5秒。
输出中蓝色的部分是在主UI线程上执行的,红色的部分是在新开启的线程上执行。可以看出调用EndInvoke之后,调用者线程会被堵塞,直至BeginInvoke所指定的方法执行完毕,也就是说如果EndInvoke是同步执行的,会引起线程堵塞。
Control的BeginInvoke\EndInvoke与Delegate的BeginInvoke\EndInvoke是不相同的,Delegate要求BeginInvoke与EndInvoke成对调用,而Control无需这样做,从接口也可以看出来,Control的BeginInvoke不需要传入回调函数,而Delegate则是必须的。
实现细节
那么Control是如何实现Invoke和BeginInvoke的哪?
首先我们要明白Windows窗口编程的一个规则,如果需要操作控件数据的话,只能通过创建该控件的线程来操作。这样做的目的是为了避免线程竞争,产生不可预知的错误。这也就是为什么在上面的例子中Invoke和BeginInvoke所指定的Delegate都会在主UI线程中执行的原因。
那么调用者线程怎么样告诉UI线程来执行相应动作哪?当然是通过SendMessage或者PostMessage向UI线程发送消息来执行了。
通过反编译Control的代码我们可以看到Invoke和BeginInvoke都是通过PostMessage来实现的,只不过Invoke调用的时候,调用者线程会持续等待直至Invoke结束。见下面的代码片段:
2 {
3 //...向UI线程发送消息,该消息会在Control的WndProc中处理。
4 UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
5
6 //...
7 // 如果是同步操作,则等待,否则之前已经返回。
8 if (!entry.IsCompleted)
9 {
10 this.WaitForWaitHandle(entry.AsyncWaitHandle);
11 }
12 //...
13 }
全文完。