前言
在富客戶端的app中,如果在主線程中運行一些長時間的任務,那么應用程序的UI就不能正常相應。因為主線程要負責消息循環,相應鼠標等事件還有展現UI。
因此我們可以開啟一個線程來格外處理需要長時間的任務,但在富客戶端中只有主線程才能更新UI的控件。
解決方法
簡單的來說,我們需要從其他的線程來更新UI線程的控件,需要將這個操作轉交給UI線程(線程marshal)。
方法1:
在底層的操作中,可以有以下的方法:
- WPF中,在element的Dispatcher類中調用BeginInvoke或者Invoke方法
- Metro中,在Dispatcher類中調用RunAsync或者Invoke方法
- Winform中,在控件中直接調用BeginInvoke或者Invoke方法
以上所有的方法的參數都是一個Delegate,用此Delegate來代表需要處理的任務:
public IAsyncResult BeginInvoke(Delegate method);
BeginInvoke/RunAsync方法是將這個 Delegate推送到UI線程的消息隊列中,這個消息隊列也就是前面提到的鼠標,鍵盤事件等隊列。
Invoke方法也是推送delegate到消息隊列,但還會一直阻塞到此delegate被UI線程處理為止。所以一般來說我們還是用BeginInvoke/RunAsync方法。
對應app來說,我們可以將其想象為一下的偽代碼:
while (!thisApplication.Ended) { wait for something to appear in message queue Got something: what kind of message is it? Keyboard/mouse message -> fire an event handler User BeginInvoke message -> execute delegate User Invoke message -> execute delegate & post result }
那接下來我們用winform來demo一下:
public partial class Form1 : Form { public Form1() { InitializeComponent(); new Thread(work).Start(); } void work() { Thread.Sleep(5000); UpdateMessage("july Luo thread Test"); } void UpdateMessage(string message) { Action action = () => lblJulyLuo.Text = message; this.BeginInvoke(action); } }
方法2
在 System.ComponentModel命名空間中,有 SynchronizationContext抽象類,此類也可以處理線程marshal。
在wpf,metro, winform中都定義了此類的子類,而且可以用SynchronizationContext.Current獲取,然后調用Post方法,可以理解為將其他線程的任務post到UI線程中。
一下為demo:
public partial class Form1 : Form { SynchronizationContext _uiSyncContext; public Form1() { InitializeComponent(); new Thread(() => work()).Start(); _uiSyncContext = SynchronizationContext.Current; } void work() { Thread.Sleep(5000); UpdateMessage("july Luo thread Test"); } void UpdateMessage(string message) { _uiSyncContext.Post(_ => lblJulyLuo.Text = message, null); } }
SynchronizationContext類還有一個Send方法,和我們上面提到的Invoke方法的作用一致。
當然了還有BackgroundWorker類,此類在內部用了SynchronizationContext,所以其也可在其他線程中更新UI線程。
方法3
在.net 4.0之后,已經有了TPL供我們方便的操作多線程,這里試用Task類也可以完成類似操作,demo如下:
public partial class Form1 : Form { Task<string> t; public Form1() { InitializeComponent(); t = Task.Run(() => work()); var awaiter = t.GetAwaiter(); awaiter.OnCompleted(() => { string message = awaiter.GetResult(); lblJulyLuo.Text = message; }); } string work() { Thread.Sleep(5000); return "july Luo thread Test"; } }
這里和上面有點不同,我們使用Task來代替多線程,而且其返回要更新的字符串,如何調用GetAwaiter方法返回需要的awaiter,最后在awaiter中的OnCompleted方法中直接更新控件。
原理就是awaiter中的OnCompleted方法會自動獲取synchronizationContext,也會將其推送到UI線程的消息隊列中。
方法4
使用TaskScheduler來marshal線程,TaskScheduler是抽象類,其負責分配管理Task對象。
Framework中有兩個實現:一個就是 default scheduler 其負責串聯CLR中的線程池,還有一個就是synchronization context scheduler,其負責解決其他線程需要更新UI控件的。
所以思路就是用Task實現多線程,然后調用其ContinueWith方法,並傳入對應的TaskScheduler:
public partial class Form1 : Form { TaskScheduler _uiScheduler; Task<string> t; public Form1() { InitializeComponent(); _uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); t = Task.Run(() => work()).ContinueWith(t => lblJulyLuo.Text = t.Result, _uiScheduler); } string work() { Thread.Sleep(5000); return "july Luo thread Test"; } }
總結
富客戶端中UI線程一直會處理着消息循環,無論使用那種方法都是將其推送到消息隊列中以便UI線程處理。