【问题标题】:C# tasks are not cancelledC# 任务未取消
【发布时间】:2016-02-17 16:25:08
【问题描述】:

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

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 秒后我想取消未完成任务的执行。

当我运行代码 Task.WaitAll 参数 3000 时,让前 3 个任务按预期完成。然后我得到Resultfalse,因为还有两个其他任务没有完成。然后我必须取消这两个任务。如果我尝试处置它们,我会收到异常消息“只能处置处于已完成状态的任务。”

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

【问题讨论】:

  • 将作业 (Sleep(8000)) 拆分为多个部分 (Sleep(100) * 80) 并在开始每个部分之前检查 IsCancelationRequest

标签: c# task-parallel-library


【解决方案1】:

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

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

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

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

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

【讨论】:

    【解决方案2】:

    如果您想取消尚未完成的任务,您需要通过合作取消来完成。目前,您的任何任务都不会监控传递给它们的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。前者返回一个已经启动的“热任务”,无需迭代集合并调用Start
    2. 确实没有必要处理Task。仅当您使用由Task 公开的WaitHandle 时才使用它,您在这里没有使用它
    3. 更喜欢使用Task.WhenAll 而不是Task.WaitAll
    4. 请在您的代码中遵循 .NET 命名约定。

    【讨论】:

    • 你的例子在技术上是正确的,但是代码量看起来很糟糕,需要一个函数
    • @StenPetrov 确实如此,但这是一个快速而肮脏的复制和更正。我不会为他重新实现这个方法,因为它看起来主要是测试和丢弃代码。如果这是要投入生产的任何东西,那么就需要进行明确的代码审查。但是,我回答的目的是传达执行此操作的技术方式,而不是实现多任务执行的正确方式。
    • Task.Delay(..., token) 替换Thread.Sleep 就像作弊一样。你能用Thread.Sleep(这是一种不支持取消的同步方法)让它工作吗?我想弄清楚我对问题的评论是否有意义。
    • @Sinatr 我不确定你所说的作弊是什么意思,这是在特定时间段内异步让步控制的事实上的方式。我认为没有必要启动一个线程然后主动阻止它。
    • 所以这不适用于Sleep。正如我所见,仍然存在与 OP 示例相同的问题。您只是解决了 OP sn-p 问题(通过用异步替换同步代码)并没有很好地解释它(尽管@YacoubMassad 的回答已经足够解释了)。这并不总是可能的(用异步方法替换某些东西,奇迹也支持取消),因此我的 cmets。
    【解决方案3】:

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

    • 通过简单地从代理返回。在许多情况下,这已经足够了;但是,以这种方式取消的任务实例会转换到 TaskStatus.RanToCompletion 状态,而不是 TaskStatus.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;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-01-06
      • 1970-01-01
      • 2018-09-05
      • 1970-01-01
      • 1970-01-01
      • 2016-05-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多