如何取消任务但等待其完成?

8

我有一个线程任务,它在循环中执行某些操作:

static void TaskAction(CancellationToken ct)
{
    while (SomeCondition())
    {
        DoSomeSeriousJob();
        ct.ThrowIfCancellationRequested();
    }
}

static void DoSomeSeriousJob()
{
    Console.WriteLine("Serious job started");
    Thread.Sleep(5000);
    Console.WriteLine("Serious job done");
}

我开启它,但在一段时间后取消:
    var cts = new CancellationTokenSource();
    var task = Task.Factory.StartNew(() => TaskAction(cts.Token), cts.Token);
    Thread.Sleep(1000);
    cts.Cancel();

这个操作必须正确完成,我不想中断它。但是我想发送一个取消请求到我的任务,并且要等待它正确完成(即到达代码中的某个点)。

我尝试了以下方法:

1. Wait(CancellationToken ct)

1. 使用 Wait(CancellationToken ct)

try
{
    task.Wait(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Task cancelled");
}
// Must be joined here.

在这种情况下,程序会立即从Wait()返回。任务会继续运行,直到执行ThrowIfCancellationRequested(),但如果主线程退出,任务也会被中断。

2. Wait()

是什么?
try
{
    task.Wait();
}
catch (OperationCanceledException)
{
    Console.WriteLine("Task cancelled");
}
catch (Exception ex)
{
    Console.WriteLine(ex.ToString());
}

这里主线程等待完成,但是最终会抛出 AggregateException 异常,其 InnerExceptionTaskCancelledException(而非 OperationCancelledException)。

3. 检查 IsCancellationRequested() 和无异常

static void TaskAction(CancellationToken ct)
{
    while (SomeCondition())
    {
        DoSomeSeriousJob();
        if (ct.IsCancellationRequested)
            break;
    }
}
// ...
task.Wait();

在这种情况下,没有异常被抛出,但任务最终的状态为“RanToCompletion”。当SomeCondition()开始返回false时,这与正确完成不可区分。
所有这些问题都有简单的解决方法,但我想知道,是不是我漏掉了什么?是否有人能给我更好的建议?

2
如果你将取消令牌传递给 Wait,那么它将取消等待。如果你想等待任务完成,请不要将令牌传递给 Wait - juharr
2
在第二点中,TaskCanceledException 是从 OperationCanceledException 派生而来的。如果您使用 await 而不是 Wait,那么 AggregateException 将被解开,您可以捕获 InnerException - Charles Mager
juharr,这是我的案例1。如果Wait(ct)不会等待的话,调用它有什么用? - greatvovan
明白了。如果令牌被取消,它会等待完成或引发“OperationCancelledException”,而不带参数的“Wait()”则会等待完成并在令牌被取消时引发“AggregateException”。 - greatvovan
在第三种情况下,如果您在观察到已请求取消后离开操作,您是否仍然认为该方法已完成?为了区分优雅和取消的执行,您可以使用 CancellationToken.ThrowIfCancellationRequested - Daniel C. Weber
2个回答

5
如果你想同步等待任务完成(或被取消),可以尝试以下方法:
cts.Cancel();
Task.Run(async () => {
    try {
        await task;
    }
    catch (OperationCanceledException ex) {
      // ...
    }
).Wait();

这样你就可以直接捕获OperationCanceledException而不是捕获AggregateException了。
编辑:
Wait(CanecllationToken)
这种方法不起作用。根据MSDN的声明:
等待任务完成执行。如果在任务完成之前取消了一个取消令牌,则等待将终止。
Wait()
您可以使用此方法,但是如您所见,应该期望AggregateException而不是OperationCanceledException。这也在方法的文档中指定。
AggregateException.InnerExceptions集合中包含TaskCanceledException对象。
因此,在此方法中,为确保操作已取消,可以检查内部异常是否包含TaskCanceledException。
检查IsCancellationRequested()和无异常
这种方法很明显没有抛出异常,您无法确定操作是否被取消。
如果您不想同步等待,则一切正常:
cts.Cancel();
try {
    await task;
}
catch (OperationCanceledException ex) {
   // ...
}

不如我想象中的美丽,但还是很有趣 :) - greatvovan
是的,优美的方式正在异步等待。 - Mehrzad Chehraz

0

试试这个:

try
{
    task.GetAwaiter().GetResult();
}
catch (OperationCanceledException)
{
    Console.WriteLine("Task cancelled");
}

你会收到一个 OperationCanceledException 而且它不会被包装在 AggregateException 中。


它可能起作用,但出于未知原因,msdn指出:“此方法旨在供编译器使用,而不是在应用程序代码中使用。” - Mehrzad Chehraz
1
@MehrzadChehraz,MSDN文档存在一些不完美和含沙射影。这种方法被广泛使用,并且作为awaiter基础设施的一部分,它不会消失。 - noseratio - open to work
@MehrzadChehraz,就你的方法而言,Task.Run显然是多余的。你可以直接使用(new Func<Task>(async () => { ... }))().Wait(); - noseratio - open to work

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