C# 任务不会被取消

C# tasks are not cancelled

我有几个任务要执行。每个任务在不同的持续时间内完成其执行。有些任务执行数据库访问,有些只是进行一些计算。我的代码具有以下结构:

var Canceller = new CancellationTokenSource();

List<Task<int>> tasks = new List<Task<int>>();

tasks.Add(new Task<int>(() => { Thread.Sleep(3000); Console.WriteLine("{0}: {1}", DateTime.Now, 3); return 3; }, Canceller.Token));
tasks.Add(new Task<int>(() => { Thread.Sleep(1000); Console.WriteLine("{0}: {1}", DateTime.Now, 1); return 1; }, Canceller.Token));
tasks.Add(new Task<int>(() => { Thread.Sleep(2000); Console.WriteLine("{0}: {1}", DateTime.Now, 2); return 2; }, Canceller.Token));
tasks.Add(new Task<int>(() => { Thread.Sleep(8000); Console.WriteLine("{0}: {1}", DateTime.Now, 8); return 8; }, Canceller.Token));
tasks.Add(new Task<int>(() => { Thread.Sleep(6000); Console.WriteLine("{0}: {1}", DateTime.Now, 6); return 6; }, Canceller.Token));

tasks.ForEach(x => x.Start());

bool Result = Task.WaitAll(tasks.Select(x => x).ToArray(), 3000);

Console.WriteLine(Result);

Canceller.Cancel();

tasks.ToList().ForEach(x => { x.Dispose(); }); // Exception here
tasks.Clear();
tasks = null;

Canceller.Dispose();
Canceller = null;

我有 5 秒的时间来开始所有这些任务。每 5 秒我调用上面的代码。在下一次调用之前,我必须确保上一次执行期间没有任务遗留下来。比方说,如果执行后过了 3 秒,我想取消未完成的任务的执行。

当我 运行 3000 的代码 Task.WaitAll 参数让前 3 个任务按预期完成。然后我得到 Result 作为 false 因为还有 2 个其他任务没有完成。那我必须取消这两个任务。如果我尝试处理它们,我会得到异常提示 "Tasks in completed state can only be disposed."

我怎样才能做到这一点?在我调用 CancellationTokenSourceCancel 方法后,这两个任务仍然执行。这里有什么问题?

首先,您几乎不应该使用 Task.Start。请改用静态 Task.Run 方法。

当您将 CancellationToken 传递给 Task.Run 或其他创建任务的 API 时,这不允许您通过请求取消来立即中止任务。如果任务中的代码抛出 OperationCanceledException 异常,这只会将任务的状态设置为 Canceled。请查看 this article.

的 CancellationToken 部分

要取消任务,任务运行的代码必须配合您。例如,如果代码在循环中执行某些操作,则该代码必须定期检查是否请求取消,如果是则抛出异常(或者如果您不希望任务被视为已取消,则只需退出循环)。 CancellationToken 中有一个名为 ThrowIfCancellationRequested 的方法就是这样做的。这当然意味着此类代码需要访问 CancellationToken 对象。这就是为什么我们有接受取消标记的方法。

再举个例子,如果任务运行的代码调用了一个数据库访问方法,你最好调用一个接受CancellationToken的方法,这样这个方法会在取消后立即尝试退出已请求。

所以综上所述,取消一个操作并不是什么神奇的事情,任务运行的代码需要配合。

如果您想取消尚未完成的任务,您需要通过合作取消来完成。目前,您的 none 个任务完全监控传递给它们的 CancellationToken

如果您在从睡眠中醒来后监视令牌,则使用同步 Thread.Sleep 监视令牌可以工作,但这不会 不会 中止任何正在进行的线程目前处于睡眠状态。相反,我提供了使用 Task.Delay 的替代方法。当你想监控令牌时,这很合适,因为它允许你将令牌传递给延迟操作本身。

异步等价物的粗略草图可能如下所示:

public async Task ExecuteAndTimeoutAsync()
{
    var canceller = new CancellationTokenSource();
    var tasks = new[]
    {
        Task.Run(async () =>
        {
            var delay = 2000;
            await Task.Delay(delay, canceller.Token);
            if (canceller.Token.IsCancellationRequested)
            {
                Console.WriteLine($"Operation with delay of {delay} cancelled");
                return -1;
            }
            Console.WriteLine("{0}: {1}", DateTime.Now, 3);
            return 3;
        }, canceller.Token),
        Task.Run(async () =>
        {
            var delay = 5000;
            await Task.Delay(, canceller.Token);
            if (canceller.Token.IsCancellationRequested)
            {
                Console.WriteLine($"Operation with delay of {delay} cancelled");
                return -1;
            }
            Console.WriteLine("{0}: {1}", DateTime.Now, 2);
            return 2;
        }, canceller.Token)
    };

    await Task.Delay(3000);
    canceller.Cancel();

    await Task.WhenAll(tasks);
}

如果无法使用异步,请考虑使用 Thread.Sleep 之后 监控给定令牌,以便您的线程知道您实际上请求了取消。

旁注:

  1. 使用 Task.Run 而不是 new Task。前一个returns一个已经开始的"hot task",不需要再迭代集合调用Start.
  2. Task实在没必要​​处理。仅当您使用由 Task 公开的 WaitHandle 时才使用它,您在此处未使用
  3. 更喜欢使用 Task.WhenAll 而不是 Task.WaitAll
  4. 请在您的代码中遵循 .NET 命名约定。

在任务 类 中,取消涉及代表可取消操作的用户委托与请求取消的代码之间的合作。成功取消涉及调用 CancellationTokenSource.Cancel() 方法的请求代码,以及用户委托及时终止操作。您可以使用以下选项之一终止操作:

  • 只需从委托中返回即可。在许多情况下,这就足够了;但是,以这种方式取消的任务实例会转换为 TaskStatus.RanToCompletion 状态,而不是 theTaskStatus.Canceled 状态。

  • 通过抛出一个 OperationCanceledException 并将请求取消的令牌传递给它。执行此操作的首选方法是使用 ThrowIfCancellationRequested() 方法。以这种方式取消的任务会转换为 Canceled 状态,调用代码可以使用该状态来验证任务是否响应了它的取消请求。

因此,您必须在任务中监听取消信号:

var Canceller = new CancellationTokenSource();
var token = Canceller.Token;

List<Task<int>> tasks = new List<Task<int>>();

tasks.Add(new Task<int>(() => { Thread.Sleep(3000); token.ThrowIfCancellationRequested(); Console.WriteLine("{0}: {1}", DateTime.Now, 3); return 3; }, token));
tasks.Add(new Task<int>(() => { Thread.Sleep(1000); token.ThrowIfCancellationRequested(); Console.WriteLine("{0}: {1}", DateTime.Now, 1); return 1; }, token));
tasks.Add(new Task<int>(() => { Thread.Sleep(2000); token.ThrowIfCancellationRequested(); Console.WriteLine("{0}: {1}", DateTime.Now, 2); return 2; }, token));
tasks.Add(new Task<int>(() => { Thread.Sleep(8000); token.ThrowIfCancellationRequested(); Console.WriteLine("{0}: {1}", DateTime.Now, 8); return 8; }, token));
tasks.Add(new Task<int>(() => { Thread.Sleep(6000); token.ThrowIfCancellationRequested(); Console.WriteLine("{0}: {1}", DateTime.Now, 6); return 6; }, token));

tasks.ForEach(x => x.Start());

bool Result = Task.WaitAll(tasks.Select(x => x).ToArray(), 3000);

Console.WriteLine(Result);

Canceller.Cancel();

try
{
    Task.WaitAll(tasks.ToArray());
}
catch (AggregateException ex)
{
    if (!(ex.InnerException is TaskCanceledException))
        throw ex.InnerException;
}

tasks.ToList().ForEach(x => { x.Dispose(); });
tasks.Clear();
tasks = null;

Canceller.Dispose();
Canceller = null;