C#任务未被取消

时间:2016-02-17 16:25:09

标签: c# task-parallel-library

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

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个任务按预期完成。然后我将Result作为false,因为其他2个任务尚未完成。然后我必须取消这两项任务。如果我试图处理它们,我会得到一个例外,说“处于完成状态的任务只能处理掉。”

我怎样才能做到这一点?在我调用Cancel CancellationTokenSource方法后,这两项任务仍然执行。这有什么不对?

3 个答案:

答案 0 :(得分:4)

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

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

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

作为另一个示例,如果任务运行的代码调用数据库访问方法,则最好调用接受CancellationToken的方法,以便在请求取消后,此类方法将尝试退出。

总而言之,取消操作并不是一件神奇的事情,因为任务运行的代码需要合作。

答案 1 :(得分: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);
}

如果无法使用async,请考虑在之后使用Thread.Sleep监控给定的令牌,以便您的线程知道您确实要求取消。

旁注:

  1. 使用Task.Run代替new Task。前者返回一个热门任务&#34;已经开始,无需迭代集合并调用Start
  2. 确实没有必要处置Task。仅在您使用WaitHandle公开的Task时才使用它,而您在此处未使用
  3. 请选择使用Task.WhenAll代替Task.WaitAll
  4. 在代码中遵循.NET命名约定。

答案 2 :(得分:0)

在Task类中,取消涉及用户委托(代表可取消操作)和请求取消的代码之间的协作。成功取消涉及请求代码调用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;