好久沒寫博客了。最近在重構代碼,碰到了很多了有意義的問題,在此和大家分享。
大家知道,在使用異步IO或者大量多線程代碼時,總會碰到同步問題,例如在其他線程調用Winform的Control相關代碼,就會導致異常。最近發現NAudio的WaveIn和WaveOut居然也需要使用UI線程的消息結構才能正常工作,他們會在后台創建不可見窗口。如果你在Socket的異步IO回調中直接調用Wave系列的方法,則會異常或者沒有任何反應。(BeginXXX系列等異步方法的回調是在線程池中某個線程中調用的)
一般Windows程序的UI線程只有一個,UI相關的更新都應該發生在UI線程上,才能保證安全。因此實際工作中,我們要學會怎么在一個非UI線程中轉向UI線程中執行代碼(或者通知UI線程執行某段代碼),下面我就說兩種基本的方法:
1. 使用WinForm的Control.Invoke
WinForm的每個Control都有一個屬性(InvokeRequired)和一個方法(Invoke)用來在UI線程執行代碼。Control.InvokeRequired屬性指示當前線程是不是創建Control的線程。所以這種方法一般有以下的結構
1 ... 2 3 if (anyControl.InvokeRequired) 4 { 5 anyControl.Invoke(new Action(delegate{ myControl.DoSomething(); })); 6 }
注意Invoke可以接受任何Delegate,但是那個Delegate必須是某一個Delegate:如果上面的代碼沒有new Action或者new MethodInvoker之類的具體轉化,C#會因為不知道用哪個Delegate而報錯,因為很多Delegate都符合這個沒參數沒返回值的signature。如果delegate有返回值,那么Invoke方法會返回那個值。Control還實現了BeginInvoke和EndInvoke執行異步調用,但一般不會用到。
這個方法有一個限制,就是你的上下文中有對UI控件的引用。很多情況下后台代碼並沒有對UI控件的引用,就很不方便。
2. 使用SynchronizationContext
System.Threading.SynchronizationContext是.Net平台下終極的線程間互相執行代碼的機制。我在這里只是講講一下它在UI線程中的用法,其他的大家就看MSDN吧:)
使用SynchronizationContext.Current可以獲得當前線程的SynchronizationContext。並不是所有的線程都有SynchronizationContext,但是WinForm的UI線程肯定有一個(微軟保證的),所以在程序開始的時候我們可以在Form的初始化代碼中獲得UI線程的Context,存在變量中,也可以傳給后台代碼。之后我們想在UI線程執行代碼(例如更新界面顯示),就可以調用下面兩個方法:
1 // 同步執行代碼,等待返回,第二個參數是方法參數,沒有的話用null 2 m_uiContext.Send(delegate {DoSomething();}, null); 3 // 異步執行代碼,立刻返回。第二個參數同上 4 m_uiContext.Post(delegate {DoSomething();}, null);
兩種方式一般沒有什么差別,因為我們盡量保持UI線程的操作盡量很少,保證UI不會Freeze,因此都會很快執行完畢。
以上簡單的介紹希望有用。如果誰還知道其他的方法,歡迎提出,嘿嘿;)