過年前的這段時間真是舒服,沒有了平時項目發版的緊張,剩下的就是只有在網上閒逛了,哈哈!
今天早上閒逛的時候,在CodeProject發現了個不錯的文章,英文好的直接去http://www.codeproject.com/KB/threads/SynchronizationContext.aspx看吧,不好,就將就的看下我的吧,呵呵!(沒有直接翻譯,不過大概的思路相同)
理解SynchronizationContext
SynchronizationContext 類是一個基類,可提供不帶同步的自由線程上下文。 此類實現的同步模型的目的是使公共語言運行庫內部的異步/同步操作能夠針對不同的異步模型采取正確的行為。此模型還簡化了托管應用程序為在不同的同步環境下正常工作而必須遵循的一些要求。同步模型的提供程序可以擴展此類並為這些方法提供自己的實現。(來自MSDN)
簡而言之就是允許一個線程和另外一個線程進行通訊,SynchronizationContext在通訊中充當傳輸者的角色。另外這裡有個地方需要清楚的,不是每個線程都附加SynchronizationContext這個對象,只有UI線程是一直擁有的。
這裡你可能有個問題:對於UI線程來說,是如何將SynchronizationContext這個對象附加到線程上的呢?!OK,我們先從下面的代碼開始,
[STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // let's check the context here var context = SynchronizationContext.Current; if (context == null) MessageBox.Show("No context for this thread"); else MessageBox.Show("We got a context"); // create a form Form1 form = new Form1(); // let's check it again after creating a form context = SynchronizationContext.Current; if (context == null) MessageBox.Show("No context for this thread"); else MessageBox.Show("We got a context"); if (context == null) MessageBox.Show("No context for this thread"); Application.Run(new Form1()); }
運行結果:
1、No context for this thread
2、We got a context
從運行結果來看,在Form1 form = new Form1()之前,SynchronizationContext對象是為空,而當實例化Form1窗體後,SynchronizationContext對象就被附加到這個線程上了。所以可以得出答案了:當Control對象被創建的同時,SynchronizationContext對象也會被創建並附加到線程上。
好的,我們既然已經基本了解了SynchronizationContext,接下來的事情就是使用它了!
如何使用SynchronizationContext
應用程序有兩個線程:線程A和線程B,不過線程B比較特殊,它屬於UI線程,當這兩個線程同時運行的時候,線程A有個需求:"修改UI對象的屬性",這時候如果你是線程A,你會如何去完成需求呢?!
第一種方式:
在線程A上面直接去操作UI對象,這是線程B說:"線程A,你真xx,你不知道我的特殊嘛!",然後直接拋給線程A一個異常信息,線程A得到異常後,一臉的無辜和無奈.....!
第二種方式:
InvokeRequired?!是的,當然沒問題。(解釋下,InvokeRequired屬性是每個Control對象都具有的屬性,它會返回true和false,當是true的時候,表示可以去修改UI對象,反之則不允許。通過InvokeRequired的實現方式如下:
using System; using System.Drawing; using System.Windows.Forms; using System.Threading; public class MyFormControl : Form { public delegate void AddListItem(String myString); public AddListItem myDelegate; private Button myButton; private Thread myThread; private ListBox myListBox; public MyFormControl() { myButton = new Button(); myListBox = new ListBox(); myButton.Location = new Point(72, 160); myButton.Size = new Size(152, 32); myButton.TabIndex = 1; myButton.Text = "Add items in list box"; myButton.Click += new EventHandler(Button_Click); myListBox.Location = new Point(48, 32); myListBox.Name = "myListBox"; myListBox.Size = new Size(200, 95); myListBox.TabIndex = 2; ClientSize = new Size(292, 273); Controls.AddRange(new Control[] {myListBox,myButton}); Text = " 'Control_Invoke' example "; myDelegate = new AddListItem(AddListItemMethod); } static void Main() { MyFormControl myForm = new MyFormControl(); myForm.ShowDialog(); } public void AddListItemMethod(String myString) { myListBox.Items.Add(myString); } private void Button_Click(object sender, EventArgs e) { myThread = new Thread(new ThreadStart(ThreadFunction)); myThread.Start(); } private void ThreadFunction() { MyThreadClass myThreadClassObject = new MyThreadClass(this); myThreadClassObject.Run(); } } public class MyThreadClass { MyFormControl myFormControl1; public MyThreadClass(MyFormControl myForm) { myFormControl1 = myForm; } String myString; public void Run() { for (int i = 1; i <= 5; i++) { myString = "Step number " + i.ToString() + " executed"; Thread.Sleep(400); // Execute the specified delegate on the thread that owns // 'myFormControl1' control's underlying window handle with // the specified list of arguments. myFormControl1.Invoke(myFormControl1.myDelegate, new Object[] {myString}); } } }
不過這裡存在一個有爭論的地方:這種方式必須通過調用Control的Invoke方法來實現,這就是說調用的地方必須有一個Control的引用存在。
看下MyThreadClass類,這個類中就存在MyFormControl的引用對象。其實如果這個類放在這裡是沒有任務不妥之處的,但是如果把MyThreadClass類放在業務層,這時候問題就出現了,從設計角度來說,業務層是不允許和UI有任何關系,所以MyFormControl的引用對象絕對不能存在於MyThreadClass類,但是不讓它存在,更新UI控件的需求就滿足不了,這種情況下,我們如何做到一種最佳方案呢!?
第三種方式:
本文的主角:SynchronizationContext登場了。解釋之前,先讓下面的代碼做下鋪墊,
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void mToolStripButtonThreads_Click(object sender, EventArgs e) { // let's see the thread id int id = Thread.CurrentThread.ManagedThreadId; Trace.WriteLine("mToolStripButtonThreads_Click thread: " + id); // grab the sync context associated to this // thread (the UI thread), and save it in uiContext // note that this context is set by the UI thread // during Form creation (outside of your control) // also note, that not every thread has a sync context attached to it. SynchronizationContext uiContext = SynchronizationContext.Current; // create a thread and associate it to the run method Thread thread = new Thread(Run); // start the thread, and pass it the UI context, // so this thread will be able to update the UI // from within the thread thread.Start(uiContext); } private void Run(object state) { // lets see the thread id int id = Thread.CurrentThread.ManagedThreadId; Trace.WriteLine("Run thread: " + id); // grab the context from the state SynchronizationContext uiContext = state as SynchronizationContext; for (int i = 0; i < 1000; i++) { // normally you would do some code here // to grab items from the database. or some long // computation Thread.Sleep(10); // use the ui context to execute the UpdateUI method, // this insure that the UpdateUI method will run on the UI thread. uiContext.Post(UpdateUI, "line " + i.ToString()); } } /// <summary> /// This method is executed on the main UI thread. /// </summary> private void UpdateUI(object state) { int id = Thread.CurrentThread.ManagedThreadId; Trace.WriteLine("UpdateUI thread:" + id); string text = state as string; mListBox.Items.Add(text); } }
運行結果:
mToolStripButtonThreads_Click thread: 10
Run thread: 3
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
...(x1000 times)
程序首先在Form1窗體的mToolStripButtonThreads_Click事件中,獲取當前的SynchronizationContext對象,然後啟動另外一個線程,並且將SynchronizationContext對象傳遞給啟動的線程,啟動的線程通過SynchronizationContext對象的Post方法來調用一個委托方法UpdateUI,因為UpdateUI是執行在主UI線程上的,所以可以通過它來修改UI上對象的信息。
怎麼樣!不錯吧,現在我們可以把Control引用給拋棄了,哈哈!
如果你去查下MSDN,會發現SynchronizationContext還有一個Send方法,Send和Post有什麼區別?
Send VS Post,以及異常處理 首先看下異常處理的情況
private void Run(object state) { // let's see the thread id int id = Thread.CurrentThread.ManagedThreadId; Trace.WriteLine("Run thread: " + id); // grab the context from the state SynchronizationContext uiContext = state as SynchronizationContext; for (int i = 0; i < 1000; i++) { Trace.WriteLine("Loop " + i.ToString()); // normally you would do some code here // to grab items from the database. or some long // computation Thread.Sleep(10); // use the ui context to execute the UpdateUI method, this insure that the // UpdateUI method will run on the UI thread. try { uiContext.Send(UpdateUI, "line " + i.ToString()); } catch (Exception e) { Trace.WriteLine(e.Message); } } } /// <summary> /// This method is executed on the main UI thread. /// </summary> private void UpdateUI(object state) { throw new Exception("Boom"); }
當你運行的時候, 你可能希望在UI線程上面去拋出,但是結果往往出忽你的意料,異常信息都在Run方法的線程上被捕獲了。這時候你可能想問:WHY?!
解釋之前,我們先看下,Send VS Post的結果:
Send 方法啟動一個同步請求以發送消息
Post 方法啟動一個異步請求以發送消息。
哈哈,異常處理的答案迎韌而解了吧!
今天就寫到這裡吧,下一篇和大家討論下SynchronizationContext是否在所有線程中都適用...