在多執行緒中如何呼叫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 int threadID = Thread.CurrentThread.GetHashCode(); Calculator calc; AddDelegate addDelegate = new AddDelegate(calc.Add); object[] arr = new object[2]; int sum = 0; /* Possible output: |
或許你並不想進行同步呼叫,因為它被打包傳送到另一個執行緒中去了。你可以通過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實現指派給它。
用來實現Synchronizer的一個重要元素是使用一個名為WorkerThread的巢狀類(nested class)。WorkerThread中有一個工作專案(work item)查詢。WorkItem類中包含方法代理和引數。Invoke()和BeginInvoke()用來將一個工作專案例項加入到查詢裡。WorkerThread新建一個.NET worker執行緒,它負責監測工作專案的查詢任務。查詢到專案之後,worker會讀取它們,然後呼叫DynamicInvoke()方法。