没有CancellationToken停止一个任务

14

我正在使用一个外部库,该库具有async方法,但没有CancellationToken的重载。

目前,我正在使用来自另一个StackOverflow问题的扩展方法来添加CancellationToken

    public async static Task HandleCancellation(this Task asyncTask, CancellationToken cancellationToken)
    {
        // Create another task that completes as soon as cancellation is requested. https://dev59.com/PGMl5IYBdhLWcg3wHj5O#18672893
        TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
        cancellationToken.Register(() =>
            tcs.TrySetCanceled(), useSynchronizationContext: false);
        Task cancellationTask = tcs.Task;

        // Create a task that completes when either the async operation completes, or
        // cancellation is requested.
        Task readyTask = await Task.WhenAny(asyncTask, cancellationTask);

        // In case of cancellation, register a continuation to observe any unhandled exceptions
        // from the asynchronous operation (once it completes). In .NET 4.0, unobserved task
        // exceptions would terminate the process.
        if (readyTask == cancellationTask)
            asyncTask.ContinueWith(_ => asyncTask.Exception,
                TaskContinuationOptions.OnlyOnFaulted |
                TaskContinuationOptions.ExecuteSynchronously);

        await readyTask;
    }

然而,底层任务仍然会执行完毕。这本来不是什么大问题,但有时底层任务永远不会完成,并且占用了我99%的CPU。
有没有办法在不终止进程的情况下“终止”这个任务?

7
你不能将任何取消处理程序注入到任务中。CancellationTokenSource.Cancel仅是一个请求,你应该将它视为“请,请,你能否取消这个操作?”而任务本身可能会遵循你愉快的取消请求,也可能不会。 - Sir Rufo
1
任务是在其自己的线程中运行的进程,如果设计者没有提供软取消其进程的方法,则唯一的选择是强制终止它,但由于它是第三方库,这可能会产生不良影响,如果我是你,我会找一个允许取消的库。 - MikeT
我所知道的唯一方法是使用旧的线程工具并调用Thread.Abort(),或者更好的方法是将缓慢的第三方代码加载到单独的应用程序域中,并在超时时卸载整个应用程序域。如果您从缓慢的第三方部分调用非托管代码,则无法解决此问题 - 唯一的解决方法是使用单独的进程。 - Ondrej Svejdar
3
解决这个问题的唯一真正方法是与该外部库的开发人员交流,并要求他添加取消支持或至少修复代码,以避免在没有完成的情况下吞噬99%的CPU。 - Evk
您可以在此处找到一个更简单的扩展方法,它创建了一个可取消的 TaskTask<TResult> 的包装器。 - Theodor Zoulias
4个回答

18

我正在使用另一个StackOverflow问题中的扩展方法。

那段代码非常老旧。

现代的AsyncEx方法是一个名为Task.WaitAsync的扩展方法,它看起来像这样:

var ct = new CancellationTokenSource(TimeSpan.FromSeconds(2)).Token;
await myTask.WaitAsync(ct);

我喜欢API的最终结果,因为更清楚地表明被取消的是等待(wait)而不是操作本身。

有没有办法在不杀死进程的情况下“终止”任务?

没有。

理想的解决方案是联系你正在使用的库的作者,并让他们添加对CancellationToken的支持。

除此之外,你会遇到“取消不可取消的操作”的场景,可以通过以下方法解决:

  • 将代码放入单独的进程中,在取消时终止该进程。这是唯一完全安全但最困难的解决方案。
  • 将代码放入单独的应用程序域中,在取消时卸载该应用程序域。这不是完全安全的;已终止的应用程序域可能会导致进程级资源泄漏。
  • 将代码放入单独的线程中,在取消时终止该线程。这甚至更不安全;已终止的线程可能会破坏程序内存。

Task.WhenAny(myTask, Task.Delay(-1, cancellationToken)) 怎么样?但是,当令牌被取消时,仍然不太好让已启动的任务在后台运行到完成。 - Bent Rasmussen
1
@BentRasmussen:那段代码可能会导致悬挂的Task.Delay。这些可能会累积并引起问题。当任务完成或令牌取消时,WaitAsync将清理所有悬挂的任务/令牌源。 - Stephen Cleary
2
Task.WaitAsync(CancellationToken) 自从 .NET 6 开始已经在 BCL 中可用了。 - undefined
这是唯一完全安全但最困难的解决方案。将代码在单独的进程中运行是安全的,因为当前进程不会被破坏。然而,如果我们将文件系统、本地或远程数据库等应用程序依赖的内容也视为状态的一部分,那么应用程序的整体状态可能仍然会变得不一致或无效,特别是在进程被突然终止的情况下。 - undefined

1

我能想到的唯一方法是更改TaskScheduler并自己管理用于任务的线程的创建。这是很多工作。

基本概念是创建自己的TaskScheduler实现,使用分配给自己的调度程序启动新任务。这样,您的调度程序就成为当前调度程序,并从此任务开始解决问题。

仍然有可能不起作用的原因。如果导致问题的任务使用默认任务调度程序创建更多任务,则仍然存在相同的问题。(Task.Run会这样做)

但是,如果它们使用async/await关键字,则您的调度程序将保持活动状态。

现在,有了您自己控制的调度程序,您可以使用Thread.Abort来终止任何任务。

为了了解实现的情况,您应该查看ThreadPoolTaskScheduler。那是调度程序的默认实现。

正如我所说,这是很多工作,但我能想到的唯一方法是杀死无法取消的任务。

为了测试是否有效,您可能只想实现ThreadPoolTaskSchedulerTaskCreationOptions.LongRunning选项的行为。因此,为每个任务生成一个新线程。


0

如您所建议的,您可以通过传递一个CancellationToken并调用Cancel来取消任务。

至于如何触发取消取决于您的应用程序的性质。

以下是几种可能的情况:

  1. 继续执行直到单击取消
  2. 在固定时间后取消
  3. 如果在固定时间内没有进展,则取消

在第一种情况下,您只需从取消按钮中取消任务,例如

private void cancel_Click(object sender, RoutedEventArgs e)
{
    ...
    cts = new CancellationTokenSource();
    await MyAsyncTask(cts.Token);
    cts.Cancel();
    ...
}

在情况2中,您可以在开始任务时启动定时器,然后使用CancelAfter在设定的时间后取消任务,例如。
private void start_Click(object sender, RoutedEventArgs e)
{
    ...
    cts = new CancellationTokenSource();
    cts.CancelAfter(30000);
    await MyAsyncTask(cts.Token);
    ...
}

在第三种情况下,你可以做一些关于进度的事情,例如。
private void start_Click(object sender, RoutedEventArgs e)
{
    ...
    Progress<int> progressIndicator = new Progress<int>(ReportProgress);
    cts = new CancellationTokenSource();
    await MyAsyncTask(progressIndicator, cts.Token);
    ...
}

void ReportProgress(int value)
{
    // Cancel if no progress
}

以下是一些有用的链接:并行编程, 任务取消, 进度和取消, 在设定时间后取消任务,以及取消任务列表


1
正如我在问题中所说,在这种情况下,我遇到了一个具有方法Task Start()的第三方库,因此没有CancellationToken可以传递。 - Tim
还有另一种情况可以使用取消操作:Windows服务。服务可能随时停止,而且服务需要停止正在进行的操作并返回(从“启动”调用中返回)。我的应用程序是一个长时间运行的进程。我想能够终止它,但如果它需要监视取消令牌,那么这与将布尔变量传递到服务中并在服务停止时设置它没有什么区别。服务在各个点监视此标志,并在看到时退出方法。这意味着代码必须在许多地方检查此标志。 - pwrgreg007
Windows服务背景:它会一直停留在“启动”方法中,直到完成。然后它处于“已停止”状态。当您在服务管理器中单击“停止”时,最终结果是终止正在运行的“启动”方法/进程。需要的是一种单方面“杀死”此启动过程及其可能生成的任何其他线程的方法,而无需监视每个循环、方法等中的标志的代码。 - pwrgreg007

0
有没有办法在不终止进程的情况下“终止”任务? 没有。引用微软的一篇博文: “那么,你能取消不可取消的操作吗?不能。”
即使你终止了进程,也不能保证操作本身会停止。一个Task,也被称为promise,是一个通用的构造,表示某个操作的完成,并且不提供关于操作是如何执行和在哪里执行的任何信息。就你所知,这个操作可能在设备驱动程序上执行,也可能在线程、独立进程、独立机器上执行,甚至可能在世界另一端的远程服务器上执行。你无法向一个从未设计为可取消的操作中注入取消行为,就像你无法向一个从未配备自毁硬件的飞行火箭中注入自毁行为一样。
让我们看一个例子:
public async Task<int> DoStuffAsync()
{
    Process p = new();
    p.StartInfo.FileName = @"C:\SomeProgram.exe";
    p.Start();
    await p.WaitForExitAsync();
    return p.ExitCode;
}

如果你启动了异步的DoStuffAsync()操作,然后在Task完成之前终止了当前进程,那么SomeProgram.exe将继续运行。所以假设你没有访问DoStuffAsync的源代码,也不知道它在内部做什么,一旦它被启动后你将无法停止它。你可以做的是要求拥有它的开发人员发布一个支持取消的新版本,或者寻找一个更好的库。

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