線程間通信
我們看下面的圖
圖1
我們來看線程間通信的原理:線程(Thread B)和線程(Thread A)通信, 首先線程A 必須實現同步上下文對象(Synchronization Context), 線程B通過調用線程A的同步上下文對象來訪問線程A,所有實現都是在同步上下文中完成的.線程B有兩種方式來實現線程間的通信.
第一種:調用線程A的同步上下文對象,阻礙當前線程,執行紅色箭頭調用,直到黃色箭頭返回(同步上下文執行完畢)才釋放當前線程. (1->2->3->5)
第二種: 調用線程A的同步上下文對象(實際上是在開啟一個新線程去執行,1->2->3->5) ,執行紅色箭頭,但並不阻礙當前線程(原有線程,1->4->5),綠色箭頭繼續執行.
文章中將會通過下面幾個類來進行介紹:
ISynchronizeInvoke 接口
SynchronizationContext 類
AsyncOperation / AsyncOperationManager 類
1. ISynchronizeInvoke 接口
我們先來看下面一段異步的代碼(Window Form控件下有1個Button/1個Label),但點擊Button的時候,執行異步調用,完成后,告訴Window Form的 Label控件Text屬性” Asynchronous End”.
Code1.1
1 delegate void DoWork(); 2 private void button1_Click(object sender, EventArgs e) 3 { 4 //輔助方法,查看當前線程 5 Debug.WriteLine(string.Format("Window Form Method.Thread ID:#{0}", 6 Thread.CurrentThread.ManagedThreadId)); 7 //Label lblStatus 屬於主線程的控件[1] 8 this.lblStatus.Text = "Asynchronous Start."; 9 //使用委托來調用異步方法 10 DoWork work = DoWorkMethod; 11 work.BeginInvoke(OnWorkCallback, work); 12 } 13 void OnWorkCallback(IAsyncResult asyncResult) 14 { 15 //輔助方法,查看當前線程 16 Debug.WriteLine(string.Format("Asynchronous Callback Method.Thread ID:#{0}", 17 Thread.CurrentThread.ManagedThreadId)); 18 DoWork work = asyncResult.AsyncState as DoWork; 19 if (work != null) 20 { 21 work.EndInvoke(asyncResult); 22 } 23 // 報錯:"線程間操作無效: 從不是創建控件“lblStatus”的線程訪問它." 24 this.lblStatus.Text = "Asynchronous End"; //上面注釋[1] 25 } 26 27 void DoWorkMethod() 28 { 29 Thread.Sleep(3000);//模擬耗時工作 30 }
運行代碼,我們在第22行報錯(異步方法體內).為什么呢?我們必須清楚的一點,在windows應用窗體應用程序中,對窗體上控件屬性的任何修改都必須在主線程中完成。不能從其他線程安全地訪問控件的方法和屬性。從Debug窗口中我們也可以看出(圖1.1).執行Button Click事件的時候,運行在線程ID =#10; 在異步的方法體內,運行在線程ID=#7.不同線程間不能直接通信.
為了解決這個問題,實現圖1.1 中 #10 和 #7 的通信,下來開始認識ISynchronizeInvoke 接口(此接口來自.Net Framework 1.0),提供3個方法1個屬性:
BeginInvoke / EndInvoke 方法 : 異步方法
Invoke 方法 : 同步方法
InvokeRequired 屬性 : 判讀來源的執行線程
下面我們看Code1.2的具體代碼來說明(對Code1.1改寫,其中Label 改為ListBox)
1 delegate void DoWork(); 2 private void button1_Click(object sender, EventArgs e) 3 { 4 //更新狀態,添加到Listbox 中 5 AddValue("Asynchronous Start."); 6 //使用委托來調用異步方法 7 DoWork work = DoWorkMethod; 8 work.BeginInvoke(OnWorkCallback, work); 9 } 10 11 void OnWorkCallback(IAsyncResult asyncResult) 12 { 13 DoWork work = asyncResult.AsyncState as DoWork; 14 if (work != null) 15 { 16 work.EndInvoke(asyncResult); 17 } 18 //(1)方法:調用Control控件的Invoke 19 //Action<string> asyncUpdateState = UpdateStatus; //Action<string> 介紹=> 附1 20 //Invoke(asyncUpdateState, "1:Asynchronous End."); 21 22 //(2)方法:直接在異步調用的線程下 23 UpdateStatus("2:Asynchronous End."); 24 } 25 26 void UpdateStatus(string input) 27 { 28 //把你需要通知的控件Control 賦值給ISynchronizeInvoke 29 //來實現線程間的通信 30 ISynchronizeInvoke async = this.listBoxStatus; 31 //使用(1)方法,InvokeRequired == false ,來源當前(Window Form)主線程 32 if (async.InvokeRequired == false) 33 AddValue(input); 34 else// 使用(2)方法 == true ,來源其他線程(異步) 35 { 36 Action<string> action = new Action<string>(status => 37 { 38 AddValue(status); 39 }); 40 //調用ISynchronizeInvoke 提供的Invoke 同步方法,阻礙線程,直到調用結束 41 //也可以使用ISynchronizeInvoke 提供的異步BeginInvoke/EndInvoke方法來實現調用. 42 async.Invoke(action, new object[] { input }); 43 } 44 } 45 46 void AddValue(string input) 47 { 48 this.listBoxStatus.Items.Add(string.Format("[(#{2}){0}]Context is null:{1}", input,Thread.CurrentContext==null, Thread.CurrentThread.ManagedThreadId)); 49 } 50 void DoWorkMethod() 51 { 52 Thread.Sleep(3000);//模擬耗時工作 53 }
圖1.2
在代碼中(UpdateStatus方法體內),我們可以看到主要是在ISynchronizeInvoke async = this.listBoxStatus;實現了線程間的通信,MSDN的解釋” 實現此接口的對象可以接收事件已發生的通知,並且可以響應有關該事件的查詢”. 並使Window Form(主線程) 下的ListBox 控件和來自異步方法(另外一個線程)的建立了通道. InvokeRequired 判斷線程的來源,如果使用(1)方法,來源於Window Form 自身Control 的Invoke方法, InvokeRequired將返回false; 來源另外線程(異步)如果使用(2)返回true.同時ISynchronizeInvoke 提供了異步(BeginInvoke+EndInvok)和同步方法(Invoke)來實現線程間通信.Invoke 就是最上面的圖1 所示的第一種 / BeginInvoke+EndInvok 是第二種.
附1:關於Action<T…> / Func (T…, TResult) (簡單的說就是”簡化后的委托”)的知識可以看MSDN的介紹.
Action<T…>: http://msdn.microsoft.com/zh-cn/library/018hxwa8.aspx
Func (T…, TResult): http://msdn.microsoft.com/zh-cn/library/bb549151.aspx
Code1.2雖然實現了線程間的通信, 回顧圖1的解釋,” 首先線程A 必須實現同步上下文對象(Synchronization Context)”, 而在Code1.2 中並沒有為Window Form(主線程)實現上下文對象,如果沒有這個對象一切都是不成立的.那么Window Form 做了些什么呢?
我們來看下面的代碼(使用Console程序):
Code1.3
1 static class Program 2 { 3 static void Main() 4 { 5 //1,在Main 主線程中運行,查看線程ID和同步上下文 6 Console.WriteLine("0.ThreadID:#{1},Synchronization Context is null?{0}", 7 SynchronizationContext.Current == null, Thread.CurrentThread.ManagedThreadId); 8 9 //2,在Main 主線程中運行,實例化空對象Test,查看線程ID和同步上下文 10 Test a = new Test(); 11 Console.WriteLine("1.ThreadID:#{1},Synchronization Context is null?{0}", 12 SynchronizationContext.Current == null, Thread.CurrentThread.ManagedThreadId); 13 14 //3,在Main 主線程中運行,實例化FormTest對象(繼承Form),查看線程ID和同步上下文 15 FormTest test = new FormTest(); 16 Console.WriteLine("2.ThreadID:#{1},Synchronization Context is null?{0}", 17 SynchronizationContext.Current == null, Thread.CurrentThread.ManagedThreadId); 18 19 //4,在新線程中運行,查看線程ID和同步上下文 20 new Thread(work).Start(); 21 22 Console.Read(); 23 } 24 static void work() 25 { 26 Console.WriteLine("3.ThreadID:#{1},Synchronization Context is null?{0}", 27 SynchronizationContext.Current == null, Thread.CurrentThread.ManagedThreadId); 28 } 29 } 30 public class FormTest : System.Windows.Forms.Form { } 31 public class Test { }
圖1.3
由代碼和圖可以看出(SynchronizationContext.Current == null 判斷同步上下文對象是否存在), 實例化FormTest 對象后(繼承System.Windows.Forms.Form),Form默認的幫我們創建了同步上下文對象,使主線程#9 具備了同步上下文對象,這就是為什么Code1.2 不用聲明同步上下文對象的原因,同時也告訴我們,開啟一個新線程#10,線程本身是沒有同步上下文對象的.
2. SynchronizationContext 類
相比ISynchronizeInvoke 接口,SynchronizationContext 類(來自.Net Framework 2.0)提供了更多的方法來操作同步上下文對象,實現線程間通信.在上面的例子中SynchronizationContext類中將由 Post/Send 方法來實現.
反編譯后我們看到:
Code2.1
1 public virtual void Post(SendOrPostCallback d, object state) 2 { 3 ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state); 4 } 5 6 public virtual void Send(SendOrPostCallback d, object state) 7 { 8 d(state); 9 }
Send = ISynchronizeInvoke 中的Invoke 同步調用.圖1中的第一種
Post = ISynchronizeInvoke 中的BeginInvoke + EndInvoke異步調用. 圖1中的第二種
改寫Code1.2的代碼(還是在WinForm 下編程).
Code2.2
1 delegate void DoWork(); 2 private void button1_Click(object sender, EventArgs e) 3 { 4 //System.Windows.Forms.Form 自動的創建默認的同步上下文對象, 5 //直接的獲取當前的同步上下文對象 6 SynchronizationContext context = SynchronizationContext.Current; 7 //更新狀態,添加到Listbox 中 8 AddValue<string>("Asynchronous Start."); 9 //使用委托來調用異步方法 10 DoWork work = DoWorkMethod; 11 work.BeginInvoke(OnWorkCallback, context); 12 13 } 14 void OnWorkCallback(IAsyncResult asyncResult) 15 { 16 AsyncResult async = (AsyncResult)asyncResult; 17 DoWork work = (DoWork)async.AsyncDelegate; 18 work.EndInvoke(asyncResult); 19 20 //更新狀態 21 UpdateStatus("Asynchronous End.", asyncResult.AsyncState); 22 } 23 void UpdateStatus(object input,object syncContext) 24 { 25 //獲取主線程(Window Form)中同步上下文對象 26 SynchronizationContext context = syncContext as SynchronizationContext; 27 //使用SynchronizationContext 類中異步Post 方法 28 SendOrPostCallback callback = new SendOrPostCallback(p => { 29 AddValue<object>(p); 30 }); 31 context.Post(callback, input);//Post 為異步,Send 為同步 32 33 } 34 void AddValue<T>(T input) 35 { 36 this.listBoxStatus.Items.Add(string.Format("[(#{2}){0}]Context is null:{1}", input, Thread.CurrentContext == null, Thread.CurrentThread.ManagedThreadId)); 37 } 38 void DoWorkMethod() 39 { 40 Thread.Sleep(3000);//模擬耗時工作 41 }
上面我們已經說過在主線程中System.Windows.Forms.Form 自動的創建默認的同步上下文對象, 這時候我們把當前的同步上下文對象通過參數的形式賦值到異步線程中,調用Post 方法來實現, Post 方法接收 SendOrPostCallback 委托和額外object state參數,在Post方法體內調用線程池的線程來實現(Code2.1).當然我們也可以直接使用Send方法.
下面我們看看線程中的代碼(在Console 下編程).
Code2.3
1 static class Program 2 { 3 static void Main() 4 { 5 Output("Main Thread Start."); 6 //為主線程創建Synchronization Context 7 var context = new SynchronizationContext(); 8 //開始一個新線程 9 Thread threadB = new Thread(work); 10 threadB.Start(context); 11 12 Console.Read(); 13 } 14 static void work(object context) 15 { 16 Output("Thread B"); 17 18 //獲取主線程中的同步上下文對象 19 SynchronizationContext sc = context as SynchronizationContext; 20 21 //異步的方式和主線程通信,並發送"Hello World". 22 sc.Post(new SendOrPostCallback(p => 23 { 24 Output(p); 25 }), "Hello World"); 26 } 27 static void Output(object value) 28 { 29 Console.WriteLine("[ThreadID:#{0}]{1}", Thread.CurrentThread.ManagedThreadId, value); 30 } 31 }
圖2.3
在主線程中因為沒有同步上下文對象,所以開始我們new SynchronizationContext(); 對象,其他和Code2.2 基本一樣.從圖2.3很好的解釋圖1的第二種調用,也說明了Post 是開啟新線程(線程池)運行的.
3. AsyncOperation / AsyncOperationManager 類
AsyncOperation / AsyncOperationManager 類是SynchronizationContext 類的進一步封裝和實現, AsyncOperationManager在創建AsyncOperation對象的時候會取得當前線程的同步上下文對象,並存儲在AsyncOperation之中,使我們訪問同步上下文對象更加容易.
Code3.1
1 public class MySynchronizedClass 2 { 3 private AsyncOperation operation; 4 public event EventHandler somethingHappened; 5 public MySynchronizedClass() 6 { 7 //創建AsyncOperation 對象,並把當前線程的同步上下文保持到AsyncOperation中. 8 operation = AsyncOperationManager.CreateOperation(null); 9 10 Thread workerThread = new Thread(new ThreadStart(DoWork)); 11 workerThread.Start(); 12 } 13 14 private void DoWork() 15 { 16 SendOrPostCallback callback = new SendOrPostCallback(state => 17 { 18 EventHandler handler = somethingHappened; 19 20 if (handler != null) 21 { 22 handler(this, EventArgs.Empty); 23 } 24 }); 25 26 operation.Post(callback, null); 27 //注意1 28 operation.OperationCompleted(); 29 } 30 }
代碼很簡單,我也不在解釋,可以參照上面所有代碼. 注意1: AsyncOperation類中實現了OperationCompleted的方法. SynchronizationContext 類中這個方法是沒有具體的代碼實現的.
總結:
文章中也非常適合線程的編程(除了異步)來實現通信, SynchronizationContext是最重要的一個,其他的擴展類,如SynchronizationContextSwitcher 等更高級的主題,具體可參考下面的鏈接. 在Winform中有個非常重要的BackgroundWorker 類,關於BackgroundWorker的文章很多,在此不做解釋了.(http://hi.baidu.com/ldy201001/blog/item/3ea946c4a586f8a08326ac1a.html)