繼上一回的 Start new task,這一次當然要Cancel 它了。
範例
為了讓範例能更貼近實際,不得不使用UI 更豐富的 WPF。(Windows Forms 呢?看來微軟愈來愈放棄它了,就漸漸忘了吧)
下面這個例子,按了Start這個鍵會開始由1增加到1000,我們希望按 Stop 後能停止。
步驟1
建置基本的 layout 及程式
xaml
<Window x:Class="TPL_CancelAnApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <StackPanel > <TextBlock x:Name="txtTotal" /> <Button x:Name="btnStart" Click="btnStart_Click">Start</Button> <Button x:Name="btnStop">Stop</Button> </StackPanel> </Grid> </Window>MainWindow.xaml.cs
private void btnStart_Click(object sender, RoutedEventArgs e) { long total = 0; while (total < 1000) { total++; Thread.Sleep(1); txtTotal.Text = total.ToString(); } }
這個步驟執行的結果不如預期。畫面一次就顯示到 1000,而逐次遞增的。試一下澴沒到1000時,程式是沒有任何回應的。這也是常見的UI 回應的問題。步驟2我們就使用 WPF 的 Dispatcter 來修正吧。
步驟2
private void btnStart_Click(object sender, RoutedEventArgs e) { Task.Factory.StartNew(() => { long total = 0; while (total < 1000) { total++; Thread.Sleep(1); Action<string> action = new Action<string>(s => txtTotal.Text = s); txtTotal.Dispatcher.BeginInvoke(action, DispatcherPriority.Normal, total.ToString()); }; }); }
這個步驟中增加了 Task.Factory.StartNew 來新增一個 Task。執行過程中,要更新 UI Thread的內容時,不能直接在非 UI Thread 的 task l來更新,故必須透過 WPF 中特殊的 Dispatcher來更新。更新的方法就是呼叫要更新的控制項的 Dispatcher.BeginInvoke,並傳入參數。第一個參數是執行更新的方法,在這裡定義在 action 中。第二個參數是更新的優先度,這裡傳入DispatcherPriority.Normal。第三個則是 action 傳入的參數,即要更新的文字。
經過步驟2,就能以即時顯示的方式來反應UI了。
步驟3
步驟3中,我們要增加 Stop 的機制。TPL 中 Task 的取消方式也很直覺。設計的方法,是在執行的任務中給一個特定的物件 CancellationTokenSource,該物件定義任務中可以用來取消任務的來源。白話點講,「誰可以取消我這個任務呢?有CancellationToken 的來源就可以取消我這個任務」。而誰可這樣的Token呢?CancellationTokenSource就有。於是當CancellationTokenSource執行Cencel()時,我這個任務就收到了Cencel 的 Request 了。
以下是步驟3的程式碼
private void btnStart_Click(object sender, RoutedEventArgs e) { Task.Factory.StartNew(() => { long total = 0; while (total < 1000) { total++; Thread.Sleep(1); Action<string> action = new Action<string>(s => txtTotal.Text = s); txtTotal.Dispatcher.BeginInvoke(action, DispatcherPriority.Normal, total.ToString()); //當收到要求取消任務時,我就 break if (tokenSource.IsCancellationRequested) break; }; }, tokenSource.Token); //具有這個 Token 的來源可以取消這個任務 } private void btnStop_Click(object sender, RoutedEventArgs e) { tokenSource.Cancel(); }
在使用 IsCancellationRequested 時,我用了 break 來離開while迴圈。在長時間的任務中,使用Iteration(迭代)回圈的方式是常見的,但非一定是這樣的處理方式。因此,未必能使用 break。為了能有效且一致方式的終止任務,TPL 使用了 throw exception 方式。
//當收到要求取消任務時,我就 break if (tokenSource.IsCancellationRequested) { //釋放資源 throw new OperationCanceledException(tokenSource.Token); //break; //被OperationCanceledException取代了 }
上面程式如果沒有要釋放資源,更可以精簡如下
//當收到要求取消任務時,我就終止任務 tokenSource.Token.ThrowIfCancellationRequested();雖然可以精簡程式,但以 code 來看,誰會將 ThrowIfCancellationRequested() 與終止任務連想在一起呢?這一點我認為設計的並不是很恰當。
結論
這個過程看起來簡單,並且被 Dispatcher 與 TPL 這兩個設計涵蓋住,但裡頭的學問可真不少呢。對於新手來說,可真地說又愛又恨。愛的是寫作變簡單了,恨的是難以了解內部是如何實作的。
範例下載
沒有留言:
張貼留言