C#任务无法取消。

3

我有几个任务需要执行,每个任务的执行时间不同。其中一些任务执行数据库访问,另一些任务只做一些计算。我的代码结构如下:

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个任务未完成。然后我必须取消这两个任务。如果我试图释放它们,我会收到异常提示“已完成的任务只能被释放”。
我该怎么做才能实现这个目标呢?在调用CancellationTokenSourceCancel方法后,这两个任务仍然在执行。问题出在哪里?

将任务(Sleep(8000))分成若干部分(Sleep(100) * 80),并在开始每一部分之前检查IsCancelationRequest - Sinatr
3个回答

4

首先,你几乎永远不应该使用Task.Start。相反,请使用静态的Task.Run方法。

当你将CancellationToken传递给Task.Run或其他创建任务的API时,这并不能立即通过请求取消来中止任务。只有当任务中的代码抛出OperationCanceledException异常时,这才会将任务的状态设置为Canceled。请参阅此文章中的CancellationToken部分。

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

另一个例子是,如果任务运行的代码调用数据库访问方法,则最好调用一个接受CancellationToken的方法,以便这样的方法在请求取消时尝试尽快退出。

因此,总之,取消操作并不是什么神奇的事情,因为任务运行的代码需要与您合作。


3
如果你想取消尚未完成的任务,你需要采用协作取消的方式。目前,你的任务都没有监视传递给它们的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命名约定。

你的例子在技术上是正确的,但代码量看起来很糟糕,需要一个函数来简化。 - Sten Petrov
1
@StenPetrov,这确实可以实现,但这只是一个快速而粗略的复制和更正。我不会为他重新实现此方法,因为它似乎主要是测试和丢弃代码。如果这是任何要投入生产的东西,一定需要进行代码审查。但是,我的答案的目的是传达技术上实现此操作的方式,而不是正确实现多任务执行的方法。 - Yuval Itzchakov
@Sinatr 我不确定你所说的"cheat"是什么意思,这是一种事实上的异步控制方式,用于在一定时间内暂时放弃控制权。我认为没有必要启动一个线程然后主动阻塞它。 - Yuval Itzchakov
所以这个方法不能与Sleep一起使用。在我看来,它与 OP 的示例有着相同的问题。你只是用异步代码替换了 OP 的片段问题,并且解释得很不好(尽管 @YacoubMassad 的回答已经足够解释清楚了)。但并不总是可能将某些东西替换为同时还支持取消的异步方法,因此我有这些评论。 - Sinatr
@Sinatr OP可以将Task.DelayThread.Sleep进行切换。不同之处在于,强制线程休眠只会在线程唤醒后允许监视令牌取消。切换到Task.Delay正好满足OP的要求,即使在执行暂停期间也允许协作取消。如果您认为我的问题解释得不好,欢迎您添加自己的答案,并详细说明您认为他的代码应该如何。回答并不总是意味着以与OP编写的代码完全相同的方式解决问题。 - Yuval Itzchakov
显示剩余2条评论

1
在任务类中,取消涉及用户委托和请求取消的代码之间的协作。成功的取消需要请求代码调用 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;

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