如何等待取消的任务完成?

24
我在使用.NET 4.0进行并行编程时显然不知所措。我有一个简单的Windows应用程序,启动一个任务来执行一些无聊的工作(输出1-1000的数字)。我在中途放了一个长时间的暂停来模拟长时间运行的过程。当这个长时间的暂停正在进行时,如果我点击Stop按钮,它的事件处理程序将调用CancellationTokenSource的Cancel方法。在取消任务完成当前迭代之前,我不想在Stop按钮的事件处理程序中执行任何进一步的处理(在本例中,输出一条消息)。如何做到这一点?我尝试在Stop按钮的事件处理程序中使用Task.WaitAll等方法,但那只会抛出未处理的AggregateException异常。以下是代码,将在按照上述方式运行时帮助说明我的问题:
  private Task t;
  private CancellationTokenSource cts;

  public Form1()
  {
     InitializeComponent();
  }

  private void startButton_Click(object sender, EventArgs e)
  {
     statusTextBox.Text = "Output started.";

     // Create the cancellation token source.
     cts = new CancellationTokenSource();

     // Create the cancellation token.
     CancellationToken ct = cts.Token;

     // Create & start worker task.
     t = Task.Factory.StartNew(() => DoWork(ct), ct);
  }

  private void DoWork(CancellationToken ct)
  {
     for (int i = 1; i <= 1000; i++)
     {
        ct.ThrowIfCancellationRequested();

        Thread.Sleep(10);  // Slow down for text box outout.
        outputTextBox.Invoke((Action)(() => outputTextBox.Text = i + Environment.NewLine));

        if (i == 500)
        {
           Thread.Sleep(5000);
        }
     }
  }

  private void stopButton_Click(object sender, EventArgs e)
  {
     cts.Cancel();

     Task.WaitAll(t);  // this doesn't work :-(

     statusTextBox.Text = "Output ended.";
  }

  private void exitButton_Click(object sender, EventArgs e)
  {
     this.Close();
  }

非常感谢您对此的帮助。提前致谢。

2个回答

10

通常情况下,您只需要使用Task.Wait(而不是WaitAll),因为它只涉及单个任务。然后适当处理异常:

private void stopButton_Click(object sender, EventArgs e)
{
    cts.Cancel();
    try
    {
        t.Wait();  // This will throw
    }
    catch (AggregateException ae)
    {
       ae.Handle<OperationCanceledException>(ce => true);
    }

    statusTextBox.Text = "Output ended.";
}

当您取消一个Task时,OperationCanceledException会被包装为一个AggregateException并在调用Wait()或尝试获取TaskResult(如果它是Task<T>)时抛出。
仅供您参考-特别是在您正在做的工作中,这是C#5简化事情的一个地方。使用新的async支持,您可以将其编写为:
// No need for "t" variable anymore 
// private Task t;


private async void startButton_Click(object sender, EventArgs e)
{
   statusTextBox.Text = "Output started.";

   // Create the cancellation token source.
   cts = new CancellationTokenSource();

   try
   {
      // Create & start worker task.
      await Task.Run(() => DoWork(cts.Token));
      statusTextBox.Text = "Output ended.";
   }
   catch(OperationCanceledException ce) 
   {
      // Note that we get "normal" exception handling
      statusTextBox.Text = "Operation canceled.";
   }
}

private void stopButton_Click(object sender, EventArgs e)
{
   // Just cancel the source - nothing else required here
   cts.Cancel();
}

我想请教您关于Task、Task<T>、async和await的一些参考资料,我看过您的博客文章,想进一步了解。您能推荐一些参考资料吗? - DarthVader
1
@DarthVader 我的博客对于异步/等待任务非常有用,可以参考http://msdn.microsoft.com/en-us/vstudio/hh378091.aspx和Jon Skeet的博客。 - Reed Copsey
非常感谢您回答我的问题,Reed,并提供有关使用C# 5的额外信息。您解释得非常透彻。再次感谢! - Mac
5
我看不到 AggregateException.Handle<T> 的通用形式。这是您编写的扩展方法吗? - Dave Tillman
1
@DaveTillman ae.Handle<OperationCanceledException>(ce => true); 应该改为:ae.Handle(x => x is OperationCanceledException); 我认为Reed可能有一个允许类型参数的扩展方法。 - Metro Smurf

4
抱歉,声誉不够,无法将问题作为评论添加到该答案中。我想问一下,如何考虑绕过方法作为答案。
回答中的代码难道不依赖于某种用户界面调整来防止用户同时启动多个后台进程吗?当然,这总是一个问题。
在这里,我提供了一种替代方案,回答了所述的问题。它展示了如何等待取消请求完成。如果没有良好管理,它仍然可以让UI出错,但是它有实际等待取消后的代码,如果真的需要的话。这是一个较大C#类的摘录:
AutoResetEvent m_TaskFinishedEvent = new AutoResetEvent( false );
private IAsyncAction m_Operation = null;

private Task WaitOnEventAsync( EventWaitHandle WaitForEvent )
{
    return Task.Run( () => { WaitForEvent.WaitOne(); } );
}

public async void Start()
{
if( m_Operation != null )
    {
        // Cancel existing task
        m_Operation.Cancel();
        // Wait for it to finish. This returns control to the UI.
        await WaitOnEventAsync( m_TaskFinishedEvent );
    }
    // Start the background task.
    m_Operation = ThreadPool.RunAsync( WorkerMethod );
}

private void WorkerMethod( IAsyncAction operation )
{
    while( m_Operation.Status != AsyncStatus.Canceled )
        ; // Add real work here.

    m_TaskFinishedEvent.Set();
}

这段代码依赖于事件对象来表示任务已经基本完成。WorkerMethod()代码尚未返回,但是当事件被触发时,所有有用的工作都已完成。
我没有提供Stop()函数,因为我使用这段代码的方式。如果代码需要这样工作,则等待代码将放在Stop()函数中。
是的,你不能使用常规的Wait()函数,因为会出现取消异常。但是被接受的答案更像是一种解决方法,无意冒犯(也许我错了?)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接