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来处理方法的同步及异步调用。

 1 public interface 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上,添加如下代码:

 1 public partial class Form1 : 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方法的意图是获取异步执行方法的返回值,如果需要使用返回值,或者被refout标识的参数,则需要被调用。

再看一下上面的例子,更改Test以及WriteMessage方法:

 1         private void Test()
 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结束。见下面的代码片段:

 1 private object MarshaledInvoke(Control caller, Delegate method, object[] args, bool synchronous)
 2 {
 3    //...向UI线程发送消息,该消息会在Control的WndProc中处理。
 4     UnsafeNativeMethods.PostMessage(new HandleRef(thisthis.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
 5     
 6    //...
 7    // 如果是同步操作,则等待,否则之前已经返回。
 8     if (!entry.IsCompleted)
 9     {
10         this.WaitForWaitHandle(entry.AsyncWaitHandle);
11     }
12    //...
13 }

 

全文完。

 

posted @ 2009-03-10 16:32  ted  阅读(1513)  评论(12编辑  收藏  举报