天天看點

C# 在多線程中如何調用Winform

問題的産生:

  我的WinForm程式中有一個用于更新主視窗的工作線程(worker thread),但文檔中卻提示我不能在多線程中調用這個form(為什麼?),而事實上我在調用時程式常常會崩掉。請問如何從多線程中調用form中的方法呢? 

  解答:

  每一個從Control類中派生出來的WinForm類(包括Control類)都是依靠底層Windows消息和一個消息泵循環(message pump loop)來執行的。消息循環都必須有一個相對應的線程,因為發送到一個window的消息實際上隻會被發送到建立該window的線程中去。其結果是,即使提供了同步(synchronization),你也無法從多線程中調用這些處理消息的方法。大多數plumbing是掩藏起來的,因為WinForm是用代理(delegate)将消息綁定到事件處理方法中的。WinForm将Windows消息轉換為一個基于代理的事件,但你還是必須注意,由于最初消息循環的緣故,隻有建立該form的線程才能調用其事件處理方法。如果你在你自己的線程中調用這些方法,則它們會在該線程中處理事件,而不是在指定的線程中進行處理。你可以從任何線程中調用任何不屬于消息處理的方法。

  Control類(及其派生類)實作了一個定義在System.ComponentModel命名空間下的接口 -- ISynchronizeInvoke,并以此來處理多線程中調用消息處理方法的問題:

public interface ISynchronizeInvoke

{

 object Invoke(Delegate method,object[] args);

 IAsyncResult BeginInvoke(Delegate method,object[] args);

 object EndInvoke(IAsyncResult result);

 bool InvokeRequired {get;}

}

  ISynchronizeInvoke提供了一個普通的标準機制用于在其他線程的對象中進行方法調用。例如,如果一個對象實作了ISynchronizeInvoke,那麼線上程T1上的用戶端可以在該對象中調用ISynchronizeInvoke的Invoke()方法。Invoke()方法的實作會阻塞(block)該線程的調用,它将調用打包發送(marshal)到 T2,并在T2中執行調用,再将傳回值發送會T1,然後傳回到T1的用戶端。Invoke()方法以一個代理來定位該方法在T2中的調用,并以一個普通的對象數組做為其參數。

  調用者還可以檢查InvokeRequired屬性,因為你既可以在同一線程中調用ISynchronizeInvoke也可以将它重新定位(redirect)到其他線程中去。如果InvokeRequired的傳回值是false的話,則調用者可以直接調用該對象的方法。

  比如,假設你想要從另一個線程中調用某個form中的Close方法,那麼你可以使用預先定義好的的MethodInvoker代理,并調用Invoke方法:

Form form;

/* obtain a reference to the form, 

then: */

ISynchronizeInvoke synchronizer;

synchronizer = form;

if(synchronizer.InvokeRequired)

{

MethodInvoker invoker = new 

MethodInvoker(form.Close);

synchronizer.Invoke(invoker,null);

}

else

form.Close();

  ISynchronizeInvoke不僅僅用于WinForm中。例如,一個Calculator類提供了将兩個數字相加的Add()方法,它就是通過ISynchronizeInvoke來實作的。使用者必須确定ISynchronizeInvoke.Invoke()方法的調用是執行在正确的線程中的。

  C# 在正确的線程中寫入調用

  清單A. Calculator類的Add()方法用于将兩個數字相加。如果使用者直接調用Add()方法,它會在該使用者的線程中執行調用,而使用者可以通過ISynchronizeInvoke.Invoke()将調用寫入正确的線程中。

  清單A:

public class Calculator : ISynchronizeInvoke

{

 public int Add(int arg1,int arg2)

 { 

  int threadID = Thread.CurrentThread.GetHashCode();

  Trace.WriteLine( "Calculator thread ID is " + threadID.ToString());

  return arg1 + arg2;

 }

 //ISynchronizeInvoke implementation 

 public object Invoke(Delegate method,object[] args)

 {

  public IAsyncResult BeginInvoke(Delegate method,object[] args)

  {

   public object EndInvoke(IAsyncResult result)

   {

    public bool InvokeRequired

    {

    }

   }

   //Client-side code

   public delegate int AddDelegate(int arg1,int arg2);

    int threadID = Thread.CurrentThread.GetHashCode();

    Trace.WriteLine("Client thread ID is " + threadID.ToString());

    Calculator calc;

    /* Some code to initialize calc */

    AddDelegate addDelegate = new AddDelegate(calc.Add);

    object[] arr = new object[2];

    arr[0] = 3;

    arr[1] = 4;

    int sum = 0;

    sum = (int) calc.Invoke(addDelegate,arr);

    Debug.Assert(sum ==7);

    /* Possible output:

    Calculator thread ID is 29

    Client thread ID is 30 

    */

  或許你并不想進行同步調用,因為它被打包發送到另一個線程中去了。你可以通過BeginInvoke()和EndInvoke()方法來實作它。你可以依照通用的.NET非同步程式設計模式(asynchronous programming model)來使用這些方法:用BeginInvoke()來發送調用,用EndInvoke()來實作等待或用于在完成時進行提示以及收集傳回結果。

  還值得一提的是ISynchronizeInvoke方法并非安全類型。 類型不符會導緻在執行時被抛出異常,而不是編譯錯誤。是以在使用ISynchronizeInvoke時要格外注意,因為編輯器無法檢查出執行錯誤。

  實作ISynchronizeInvoke要求你使用一個代理來在後期綁定(late binding)中動态地調用方法。每一種代理類型均提供DynamicInvoke()方法: public object DynamicInvoke(object[] 

args);

  理論上來說,你必須将一個方法代理放到一個需要提供對象運作的真實的線程中去,并使Invoke() 和BeginInvoke()方法中的代理中調用DynamicInvoke()方法。ISynchronizeInvoke的實作是一個非同一般的程式設計技巧,本文附帶的源檔案中包含了一個名為Synchronizer的幫助類(helper class)和一個測試程式,這個測試程式是用來論證清單A中的Calculator類是如何用Synchronizer類來實作ISynchronizeInvoke的。Synchronizer是ISynchronizeInvoke的一個普通實作,你可以使用它的派生類或者将其本身作為一個對象來使用,并将ISynchronizeInvoke實作指派給它。 

繼續閱讀