任务取消和任务继续选项

4

昨天我刚接触到Tasks(TPL),于是我尝试做一个小样例项目来理解如何使用它们。

我的样例项目设置了一个开始按钮,该按钮开始递增一个进度条。第二个按钮用于取消任务。一个文本框用于报告使用TaskContinuationOptions.OnlyOnRanToCompletion进行继续时的情况,以及一个文本框用于报告使用TaskContinuationOptions.OnlyOnCanceled进行继续时的情况。

我可以创建和执行一个任务,但是以一种允许使用TaskContinuationOptions.OnlyOnCanceled标志触发继续的方式取消它一直是个问题。

我创建任务的方式如下:

private void StartTask()
{
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;

    Task task = null;
    task = Task.Factory.StartNew(() => DoWork(tokenSource), tokenSource.Token);

    //A list<CancellationTokenSource> so that I can cancel the task when clicking a button on the UI Thread.
    MyTasks.Add(tokenSource);

    Task completed = task.ContinueWith(result => TaskCompleted(), TaskContinuationOptions.OnlyOnRanToCompletion);
    Task canceled = task.ContinueWith(result => TaskCanceled(), TaskContinuationOptions.OnlyOnCanceled);
}

我按照以下步骤取消任务:

private void CancelTasks()
{
    foreach (CancellationTokenSource tokenSource in MyTasks)
    {
        tokenSource.Cancel();                
    }
}

我的工作函数如下:

private void DoWork(CancellationTokenSource tokenSource)
{
    if (progressBar1.InvokeRequired)
    {
        progressBar1.Invoke(new Action(() => DoWork(tokenSource)));
        return;
    }

    try
    {
        bool dowork = true;
        while (dowork)
        {
            tokenSource.Token.ThrowIfCancellationRequested();

            if (progressBar1.Value == progressBar1.Maximum)
            {
                dowork = false;
            }
            Thread.Sleep(1000);
            progressBar1.PerformStep();
            Application.DoEvents();
        }
        countCompleted++;
    }
    catch (OperationCanceledException)
    {                
    }
}

在我阅读的其他帖子中,有人建议使用 tokenSource.Token.ThrowIfCancellationRequested() 来设置 TaskContinuationOptions.OnlyOnCanceled 评估的条件。
但我看到的所有示例都没有使用:
catch (OperationCanceledException)
{
}

然而,如果没有它,当我调用tokenSource.Cancel()时程序就会停止。

现在的情况是,当我调用tokenSource.Cancel()时,具有TaskContinuationOptions.OnlyOnRanToCompletion选项的Continuation执行,而不是TaskContinuationOptions.OnlyOnCanceled。

显然,我做得并不正确。

编辑

进一步阅读后,我发现有一个评论指出:

"catch (OperationCanceledException) {}将把任务的状态设置为RanToCompletion而不是Canceled"

因此,移除catch(OperationCanceledException){}可以将任务的状态设置为已取消,但程序会在tokenSource.Token.ThrowIfCancellationRequested()处中断。如果我继续执行,则会运行带有TaskContinuationOptions.OnlyOnCanceled选项的连续任务,这很好。

但是,我如何调用tokenSource.Token.ThrowIfCancellationRequested(),使程序不会中断,并允许任务状态设置为已取消?


1
不要调用ThrowIfCancellationRequested(),而是检查IsCancellationRequested属性并跳出循环? - Jon
1
@Jon 当我尝试这样做时,它确实允许我跳出循环,但任务的状态返回为RanToCompletion,而不是canceled,因此会调用错误的继续操作。 - Blau
好的,我找到了另一篇帖子,有人遇到了类似的问题;他们说他们意识到他们是在调试模式下启动程序,而当不使用调试运行时,它可以正常工作。所以我尝试了一下,一切似乎都正常工作了。但这意味着我不能在调试模式下运行其余部分的程序,否则就必须不断处理这个异常被抛出的问题。有没有办法在调试模式下使其正常工作? - Blau
只需配置调试器以不中断该异常类型。在调试菜单中,您可以选择要中断和不中断的确切异常。 - Servy
@Servy 非常感谢。我以前从未遇到过这种情况,但是这解决了我的问题。我认为通过抛出异常而不是仅设置属性来让继续任务知道任务已被取消的方式有点奇怪......但是我还是编程新手,所以我不知道它是否是一种优秀的方法,具有目前超出我理解的好处。无论如何,再次感谢您花时间帮助我学习新东西。 - Blau
另一种方法是 工具 > 选项 > 调试 > 然后取消选中 "启用仅限我的代码" 选项。在此处找到此提示:如何:取消任务及其子级,在示例代码的最后评论中。 - Adrian Torrie
1个回答

10

以上评论在调试器和防止调试器断开所需选项方面是正确的。但是,以下内容应该为您提供如何使用continuations以及如何处理在这些continuations中的任务抛出的异常的更好示例...

一个 continuation 可以通过前置的任务的异常属性找出是否有异常被抛出。以下代码将将一个 NullReferenceException 打印到控制台。

Task task1 = Task.Factory.StartNew (() => { throw null; });
Task task2 = task1.ContinueWith (ant => Console.Write(ant.Exception());

如果 task1 抛出异常并且该异常未被续体捕获/查询,则被视为未处理的异常,导致应用程序崩溃。使用续体,仅通过 Status 关键字即可建立任务的结果。

asyncTask.ContinueWith(task =>
{
    // Check task status.
    switch (task.Status)
    {
        // Handle any exceptions to prevent UnobservedTaskException.             
        case TaskStatus.RanToCompletion:
            if (asyncTask.Result)
            {
                // Do stuff...
            }
            break;
        case TaskStatus.Faulted:
            if (task.Exception != null)
                mainForm.progressRightLabelText = task.Exception.InnerException.Message;
            else
                mainForm.progressRightLabelText = "Operation failed!";
        default:
            break;
    }
}

如果您不使用continuations,您要么必须在try/catch块中等待任务,要么在try/catch块中查询任务的Result

int x = 0;
Task<int> task = Task.Factory.StartNew (() => 7 / x);
try
{
    task.Wait();
    // OR.
    int result = task.Result;
}
catch (AggregateException aggEx)
{
    Console.WriteLine(aggEx.InnerException.Message);
}
希望这有所帮助。

Hope this helps.

=>

希望这有所帮助。


最终的catch(AggregateException aggEx)应该处理多个异常和嵌套的AggregateExceptions,类似于以下代码: foreach(Exception innerEx in aggEx.Flatten().InnerExceptions) { Console.WriteLine( innerEx.Message); } - Simon Giles

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