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."
我怎样才能做到这一点?在我调用 CancellationTokenSource
的 Cancel
方法后,这两个任务仍然执行。这里有什么问题?
首先,您几乎不应该使用 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
在 之后 监控给定令牌,以便您的线程知道您实际上请求了取消。
旁注:
- 使用
Task.Run
而不是 new Task
。前一个returns一个已经开始的"hot task",不需要再迭代集合调用Start
.
Task
实在没必要处理。仅当您使用由 Task
公开的 WaitHandle
时才使用它,您在此处未使用
- 更喜欢使用
Task.WhenAll
而不是 Task.WaitAll
。
- 请在您的代码中遵循 .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;
我有几个任务要执行。每个任务在不同的持续时间内完成其执行。有些任务执行数据库访问,有些只是进行一些计算。我的代码具有以下结构:
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."
我怎样才能做到这一点?在我调用 CancellationTokenSource
的 Cancel
方法后,这两个任务仍然执行。这里有什么问题?
首先,您几乎不应该使用 Task.Start
。请改用静态 Task.Run
方法。
当您将 CancellationToken
传递给 Task.Run
或其他创建任务的 API 时,这不允许您通过请求取消来立即中止任务。如果任务中的代码抛出 OperationCanceledException
异常,这只会将任务的状态设置为 Canceled
。请查看 this article.
要取消任务,任务运行的代码必须配合您。例如,如果代码在循环中执行某些操作,则该代码必须定期检查是否请求取消,如果是则抛出异常(或者如果您不希望任务被视为已取消,则只需退出循环)。 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
在 之后 监控给定令牌,以便您的线程知道您实际上请求了取消。
旁注:
- 使用
Task.Run
而不是new Task
。前一个returns一个已经开始的"hot task",不需要再迭代集合调用Start
. Task
实在没必要处理。仅当您使用由Task
公开的WaitHandle
时才使用它,您在此处未使用- 更喜欢使用
Task.WhenAll
而不是Task.WaitAll
。 - 请在您的代码中遵循 .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;