繁体   English   中英

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

当我运行代码Task.WaitAll 3000时,使前3个任务按预期完成。 然后我得到的Resultfalse因为其他2个任务尚未完成。 然后,我必须取消这两个任务。 如果我要处置它们,我会说“只能完成处置状态的任务”。

我该如何实现? 在我调用CancellationTokenSource Cancel方法之后,这两个任务仍然执行。 怎么了

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

当您将CancellationToken传递给Task.Run或其他创建任务的API时,这不允许您通过请求取消立即中止任务。 仅当任务中的代码引发OperationCanceledException异常时,才将任务的状态设置为“ Canceled OperationCanceledException 请查看本文的CancellationToken部分。

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

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

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

如果要取消尚未完成的任务,则需要通过协作取消来取消。 当前,您的任何任务都无法监视传递给他们的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命名约定。

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

  • 通过简单地从代表那里返回。 在许多情况下,这就足够了; 但是,以这种方式取消的任务实例将转换为TaskStatus.RanToCompletion状态,而不是TaskStatus.Canceled状态。

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

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

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;

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM