2010年6月14日 星期一

TPL 學習日記(2): Cancel a task

繼上一回的 Start new task,這一次當然要Cancel 它了。

範例

為了讓範例能更貼近實際,不得不使用UI 更豐富的 WPF。(Windows Forms 呢?看來微軟愈來愈放棄它了,就漸漸忘了吧)

下面這個例子,按了Start這個鍵會開始由1增加到1000,我們希望按 Stop 後能停止。

image

步驟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 這兩個設計涵蓋住,但裡頭的學問可真不少呢。對於新手來說,可真地說又愛又恨。愛的是寫作變簡單了,恨的是難以了解內部是如何實作的。

範例下載

沒有留言:

Share with Facebook