1. 程式人生 > 其它 >C#委託Delegate、Invoke和BeginInvoke及訊息機制

C#委託Delegate、Invoke和BeginInvoke及訊息機制

Invoke和BeginInvoke機制

Windows程式訊息機制

Windows GUI程式是基於訊息機制的,有個主執行緒維護著一個訊息泵。這個訊息泵讓windows程式生生不息。

Windows程式有個訊息佇列,窗體上的所有訊息是這個佇列裡面訊息的最主要來源。這裡的while迴圈使用了GetMessage()這個方法,這是個阻塞方法,也就是佇列為空時方法就會被阻塞,從而這個while迴圈停止運動,這避免了一個程式把cpu無緣無故地耗盡,讓其它程式難以得到響應。
當然在某些需要cpu最大限度運動的程式裡面就可以使用另外的方法,例如某些3d遊戲或者及時戰略遊戲中,一般會使用PeekMessage()

這個方法,它不會被windows阻塞,從而保證整個遊戲的流暢和比較高的幀速。

這個主執行緒維護著整個窗體以及上面的子控制元件。當它得到一個訊息,就會呼叫DispatchMessage方法派遣訊息,這會引起對窗體上的視窗過程的呼叫。視窗過程裡面當然是程式設計師提供的窗體資料更新程式碼和其它程式碼。

.Net訊息迴圈

public static void Main(string[] args)
{
  Form f = new Form();
  Application.Run(f);
}

.Net窗體程式封裝了上述的while迴圈,這個迴圈就是通過Application.Run方法啟動的。

執行緒外操作GUI控制元件的問題

如果從另外一個執行緒操作windows窗體上的控制元件,就會和主執行緒產生競爭,造成不可預料的結果,甚至死鎖。因此windows GUI程式設計有一個規則,就是隻能通過建立控制元件的執行緒來操作控制元件的資料,否則就可能產生不可預料的結果。

因此,.Net裡面,為了方便地解決這些問題,Control類實現了ISynchronizeInvoke介面,提供了Invoke和BeginInvoke方法來提供讓其它執行緒更新GUI介面控制元件的機制。

public interface ISynchronizeInvoke
{
  [HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]
  IAsyncResult BeginInvoke(Delegate method, object[] args);
  object EndInvoke(IAsyncResult result);
  object Invoke(Delegate method, object[] args);
  bool InvokeRequired { get; }
}

如果從執行緒外操作windows窗體控制元件,那麼就需要使用Invoke或者BeginInvoke方法,通過一個委託把呼叫封送到控制元件所屬的執行緒上執行。

訊息機制---執行緒間和程序間通訊機制

window訊息傳送

Windows訊息機制是windows平臺上的執行緒或者程序間通訊機制之一。Windows訊息值其實就是定義的一個數據結構,最重要的是訊息的型別,它就是一個整數;然後就是訊息的引數。訊息的引數可以表示很多東西。

Windows提供了一些api用來向一個執行緒的訊息佇列傳送訊息。因此,一個執行緒可以向另一個執行緒的訊息佇列傳送訊息從而告訴對方做什麼,這樣就完成了執行緒間的通訊。有些api傳送訊息需要一個視窗控制代碼,這種函式可以把訊息傳送到指定視窗的主執行緒訊息佇列;而有些則可以直接通過執行緒控制代碼,把訊息傳送到該執行緒訊息佇列中。

SendMessage是windows api,用來把一個訊息傳送到一個視窗的訊息佇列。這個方法是個阻塞方法,也就是作業系統會確保訊息的確傳送到目的訊息佇列,並且該訊息被處理完畢以後,該函式才返回。返回之前,呼叫者將會被暫時阻塞。
PostMessage也是一個用來發送訊息到視窗訊息佇列的api函式,但這個方法是非阻塞的。也就是它會馬上返回,而不管訊息是否真的傳送到目的地,也就是呼叫者不會被阻塞。

Invoke and BeginInvoke


Invoke或者BeginInvoke方法都需要一個委託物件作為引數。委託類似於回撥函式的地址,因此呼叫者通過這兩個方法就可以把需要呼叫的函式地址封送給介面執行緒。這些方法裡面如果包含了更改控制元件狀態的程式碼,那麼由於最終執行這個方法的是介面執行緒,從而避免了競爭條件,避免了不可預料的問題。如果其它執行緒直接操作介面執行緒所屬的控制元件,那麼將會產生競爭條件,造成不可預料的結果。

使用Invoke完成一個委託方法的封送,就類似於使用SendMessage方法來給介面執行緒傳送訊息,是一個同步方法。也就是說在Invoke封送的方法被執行完畢前,Invoke方法不會返回,從而呼叫者執行緒將被阻塞。

使用BeginInvoke方法封送一個委託方法,類似於使用PostMessage進行通訊,這是一個非同步方法。也就是該方法封送完畢後馬上返回,不會等待委託方法的執行結束,呼叫者執行緒將不會被阻塞。但是呼叫者也可以使用EndInvoke方法或者其它類似WaitHandle機制等待非同步操作的完成。

但是在內部實現上,Invoke和BeginInvoke都是用了PostMessage方法,從而避免了SendMessage帶來的問題。而Invoke方法的同步阻塞是靠WaitHandle機制來完成的。

使用場合

如果你的後臺執行緒在更新一個UI控制元件的狀態後不需要等待,而是要繼續往下處理,那麼你就應該使用BeginInvoke來進行非同步處理。
如果你的後臺執行緒需要操作UI控制元件,並且需要等到該操作執行完畢才能繼續執行,那麼你就應該使用Invoke。否則,在後臺執行緒和主截面執行緒共享某些狀態資料的情況下,如果不同步呼叫,而是各自繼續執行的話,可能會造成執行序列上的問題,雖然不發生死鎖,但是會出現不可預料的顯示結果或者資料處理錯誤。
可以看到ISynchronizeInvoke有一個屬性,InvokeRequired。這個屬性就是用來在程式設計的時候確定,一個物件訪問UI控制元件的時候是否需要使用Invoke或者BeginInvoke來進行封送。如果不需要那麼就可以直接更新。在呼叫者物件和UI物件同屬一個執行緒的時候這個屬性返回false。在後面的程式碼分析中我們可以看到,Control類對這一屬性的實現就是在判斷呼叫者和控制元件是否屬於同一個執行緒的。

Delegate.BeginInvoke

通過一個委託來進行同步方法的非同步呼叫,也是.net提供的非同步呼叫機制之一。但是Delegate.BeginInvoke方法是從ThreadPool取出一個執行緒來執行這個方法,以獲得非同步執行效果的。也就是說,如果採用這種方式提交多個非同步委託,那麼這些呼叫的順序無法得到保證。而且由於是使用執行緒池裡面的執行緒來完成任務,使用頻繁,會對系統的效能造成影響。
Delegate.BeginInvoke也是講一個委託方法封送到其它執行緒,從而通過非同步機制執行一個方法。呼叫者執行緒則可以在完成封送以後去繼續它的工作。但是這個方法封送到的最終執行執行緒是執行庫從ThreadPool裡面選取的一個執行緒。
這裡需要糾正一個誤區,那就是Control類上的非同步呼叫BeginInvoke並沒有開闢新的執行緒完成委託任務,而是讓介面控制元件的所屬執行緒完成委託任務的。看來非同步操作就是開闢新執行緒的說法不一定準確。

用Reflector察看一些相關程式碼

Control.BeginInvoke and Control.Invoke

public IAsyncResult BeginInvoke(Delegate method, params object[] args)
{
  using (new MultithreadSafeCallScope())
  {
    return (IAsyncResult) this.FindMarshalingControl().MarshaledInvoke(this, method, args, false);
  }
}
public object Invoke(Delegate method, params object[] args)
{
  using (new MultithreadSafeCallScope()){
    return this.FindMarshalingControl().MarshaledInvoke(this, method, args, true);
  }
}

這裡的FindMarshalingControl方法通過一個迴圈向上回溯,從當前控制元件開始回溯父控制元件,直到找到最頂級的父控制元件,用它作為封送物件。例如,我們呼叫窗體上一個進度條的Invoke方法封送委託,但是實際上會回溯到主窗體,通過這個控制元件物件來封送委託。因為主窗體是主執行緒訊息佇列相關的,傳送給主窗體的訊息才能傳送到介面主執行緒訊息佇列。

我們可以看到Invoke和BeginInvoke方法使用了同樣的實現,只是MarshaledInvoke方法的最後一個引數值不一樣。

MarshaledInvoke

private object MarshaledInvoke(Control caller, Delegate method, object[] args, bool synchronous)
{
  int num;
  if (!this.IsHandleCreated)
  {
    throw new InvalidOperationException(SR.GetString("ErrorNoMarshalingThread"));
  }
  if (((ActiveXImpl) this.Properties.GetObject(PropActiveXImpl)) != null)
  {
    IntSecurity.UnmanagedCode.Demand();
  }
  bool flag = false;
  if ((SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, this.Handle), out num) == SafeNativeMethods.GetCurrentThreadId()) && synchronous)
  {
    flag = true;
  }
  ExecutionContext executionContext = null;
  if (!flag)
  {
    executionContext = ExecutionContext.Capture();
  }
  ThreadMethodEntry entry = new ThreadMethodEntry(caller, method, args, synchronous, executionContext);
  lock (this)
  {
    if (this.threadCallbackList == null)
    {
      this.threadCallbackList = new Queue();
    }
  }
  lock (this.threadCallbackList)
  {
    if (threadCallbackMessage == 0)
    {
      threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage");
    }
    this.threadCallbackList.Enqueue(entry);
  }
  if (flag)
  {
    this.InvokeMarshaledCallbacks();
  }
  else
  { 
   //終於找到你了,
    PostMessageUnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
  }
  if (!synchronous) 
  //如果是非同步,那麼馬上返回吧
  {
    return entry;
  }
  if (!entry.IsCompleted) 
  //同步呼叫沒結束,阻塞起來等待吧
  {
    this.WaitForWaitHandle(entry.AsyncWaitHandle);
  }
  if (entry.exception != null)
  {
    throw entry.exception;
  }
  return entry.retVal;
}

怎麼樣,我們終於看到PostMessage了吧?通過windows訊息機制實現了封送。而需要封送的委託方法作為訊息的引數進行了傳遞。關於其它的程式碼這裡不作進一步解釋。

InvokeRequired

public bool InvokeRequired
{
  get
  {
    using (new MultithreadSafeCallScope())
    {
      HandleRef ref2;
      int num;
      if (this.IsHandleCreated)
      {
        ref2 = new HandleRef(this, this.Handle);
      }
      else
      {
        Control wrapper = this.FindMarshalingControl();
        if (!wrapper.IsHandleCreated)
        {
          return false;
        }
        ref2 = new HandleRef(wrapper, wrapper.Handle);
      }
      int windowThreadProcessId = SafeNativeMethods.GetWindowThreadProcessId(ref2, out num);
      int currentThreadId = SafeNativeMethods.GetCurrentThreadId();
      return (windowThreadProcessId != currentThreadId);
    }
  }
}

終於看到了,這是在判斷windows窗體執行緒和當前的呼叫者執行緒是否是同一個,如果是同一個就沒有必要封送了,直接訪問這個GUI控制元件吧。否則,就不要那麼直接表白了,就需要Invoke或者BeginInvoke做媒了。