通过在取消令牌上注册回调来取消多个任务
Cancelling multiple tasks by registering callbacks on cancellation tokens
我有以下代码片段,输出如下。
我期待第二个任务被取消,因为它还在取消令牌上注册了一个回调。
但取消只发生在第一个任务上,即最初取消的地方。
取消不应该传播到所有令牌实例吗?
Microsoft article on Cancellation Tokens 没有很好地解释这一点。
关于为什么会发生这种情况的任何指示?
代码:
class Program
{
static void Main(string[] args)
{
AsyncProgramming();
Console.ReadLine();
}
private static async void AsyncProgramming()
{
try
{
using (var cts = new CancellationTokenSource())
{
var task2 = CreateTask2(cts);
var task1 = CreateTask1(cts);
Thread.Sleep(5000);
await Task.WhenAll(task2, task1);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.WriteLine("Both tasks over");
}
private static async Task CreateTask1(CancellationTokenSource cts)
{
try
{
cts.Token.Register(() => { cts.Token.ThrowIfCancellationRequested(); });
await Task.Delay(5000);
Console.WriteLine("This is task one");
cts.Cancel();
Console.WriteLine("This should not be printed because the task was cancelled");
}
catch (Exception e)
{
Console.WriteLine("Task 1 exception: " + e.Message);
Console.WriteLine("Task 1 was cancelled");
}
}
private static async Task CreateTask2(CancellationTokenSource cts)
{
try
{
cts.Token.Register(() =>
{
Console.WriteLine("Write something");
Thread.CurrentThread.Abort();
cts.Token.ThrowIfCancellationRequested();
});
await Task.Delay(8000);
Console.WriteLine("This is task two");
}
catch (Exception e)
{
Console.WriteLine("Task 2 was cancelled by Task 1");
Console.WriteLine(e);
}
}
}
输出:
This is task one
Write something
Task 1 exception: Thread was being aborted.
Task 1 was cancelled
This is task two
Thread was being aborted.
Both tasks over
取消失败的不仅仅是第二个任务。令牌的两个注册都有效并且 ThrowIfCancellationRequested
都触发了,但是它们没有被处理,因为它们 运行 在不同的线程中。
这在后台发生(两次):
An exception of type 'System.OperationCanceledException' occurred in mscorlib.dll but was not handled in user code
您应该做的是在您的函数中调用 cts.Token.ThrowIfCancellationRequested();
而不是注册事件。
参见 https://docs.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads
中的示例
现在您正在组合两种取消方式:注册到令牌取消事件 (Token.Register
),如果令牌被取消则抛出 (Token.ThrowIfCancellationRequested
)。
要么您订阅取消事件并执行您自己的 cancel/cleanup 逻辑,要么在您的函数代码中检查是否应该取消您的操作。
示例如下所示:
private static async Task CreateTask2(CancellationToken token)
{
try
{
// Pass on the token when calling other functions.
await Task.Delay(8000, token);
// And manually check during long operations.
for (int i = 0; i < 10000; i++)
{
// Do we need to cancel?
token.ThrowIfCancellationRequested();
// Simulating work.
Thread.SpinWait(5000);
}
Console.WriteLine("This is task two");
}
catch (Exception e)
{
Console.WriteLine("Task 2 was cancelled by Task 1");
Console.WriteLine(e);
}
}
通过 Register
注册委托只是一种通知令牌何时进入取消状态的方式,仅此而已。为了进行取消,您需要在代码中对此通知做出反应,并且当您要取消的执行进入未验证取消令牌的阶段时(例如,因为正在执行的方法不接受) CancellationToken
作为参数),但您仍然需要对取消状态进行一些控制。但是在所有情况下,当您处理可以访问 CancellationToken
的代码的执行时,您不需要订阅取消通知。
在你的例子中,第一个委托引发了异常,这个异常被传播到 Cancel
调用,这就是任务被取消的原因,但这是不正确的设计,因为你不应该处理 CancellationTokenSource
在你的任务中,不应该在那里发起取消,所以我想说第一次取消只是巧合。对于第二个任务,委托被调用,但没有触发任务内部的取消,所以为什么要取消它?
第一件事是,当您调用 CancellationToken.Register
时,它通常所做的只是存储委托以供稍后调用。
thread/logic 流程调用 CancellationTokenSource.Cancel
运行 所有以前注册的代表,无论这些代表是从哪里注册的。这意味着那些通常抛出的任何异常与调用 Register.
的方法没有任何关系
旁注 1: 我在上面说 通常 ,因为有一种情况 Register
的调用将 运行代表马上。我认为这就是 msdn 文档特别混乱的原因。具体来说:如果令牌已经被取消,那么 Register
将立即 运行 委托,而不是稍后将其存储为 运行。在下面发生在 CancellationTokenSource.InternalRegister
.
完成图片的第二件事是 CancellationToken.ThrowIfCancellationRequested
所做的就是在 运行 来自的任何地方抛出异常。那通常是 CancellationTokenSource.Cancel
被调用的地方。请注意,通常所有已注册的代表都是 运行,即使其中一些会引发异常。
旁注 2: 抛出 ThreadAbortException
会更改 Cancel
方法中的预期逻辑,因为无法捕获该特殊异常。面对这种情况时,取消会停止 运行ning 任何进一步的代表。即使在捕获异常时,调用代码也会发生同样的情况。
最后要注意的是,CancellationToken 的存在不会影响方法的逻辑流。方法中的所有行 运行,除非有代码明确退出该方法,例如,通过抛出异常。如果您将取消令牌传递给 Task.Delay 调用并且它在时间过去之前从其他地方被取消,就会发生这种情况。如果您在方法中的特定行之后调用 CancellationToken.ThrowIfCancellationRequested
,也会发生这种情况。
我有以下代码片段,输出如下。 我期待第二个任务被取消,因为它还在取消令牌上注册了一个回调。 但取消只发生在第一个任务上,即最初取消的地方。 取消不应该传播到所有令牌实例吗? Microsoft article on Cancellation Tokens 没有很好地解释这一点。
关于为什么会发生这种情况的任何指示?
代码:
class Program
{
static void Main(string[] args)
{
AsyncProgramming();
Console.ReadLine();
}
private static async void AsyncProgramming()
{
try
{
using (var cts = new CancellationTokenSource())
{
var task2 = CreateTask2(cts);
var task1 = CreateTask1(cts);
Thread.Sleep(5000);
await Task.WhenAll(task2, task1);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.WriteLine("Both tasks over");
}
private static async Task CreateTask1(CancellationTokenSource cts)
{
try
{
cts.Token.Register(() => { cts.Token.ThrowIfCancellationRequested(); });
await Task.Delay(5000);
Console.WriteLine("This is task one");
cts.Cancel();
Console.WriteLine("This should not be printed because the task was cancelled");
}
catch (Exception e)
{
Console.WriteLine("Task 1 exception: " + e.Message);
Console.WriteLine("Task 1 was cancelled");
}
}
private static async Task CreateTask2(CancellationTokenSource cts)
{
try
{
cts.Token.Register(() =>
{
Console.WriteLine("Write something");
Thread.CurrentThread.Abort();
cts.Token.ThrowIfCancellationRequested();
});
await Task.Delay(8000);
Console.WriteLine("This is task two");
}
catch (Exception e)
{
Console.WriteLine("Task 2 was cancelled by Task 1");
Console.WriteLine(e);
}
}
}
输出:
This is task one
Write something
Task 1 exception: Thread was being aborted.
Task 1 was cancelled
This is task two
Thread was being aborted.
Both tasks over
取消失败的不仅仅是第二个任务。令牌的两个注册都有效并且 ThrowIfCancellationRequested
都触发了,但是它们没有被处理,因为它们 运行 在不同的线程中。
这在后台发生(两次):
An exception of type 'System.OperationCanceledException' occurred in mscorlib.dll but was not handled in user code
您应该做的是在您的函数中调用 cts.Token.ThrowIfCancellationRequested();
而不是注册事件。
参见 https://docs.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads
中的示例现在您正在组合两种取消方式:注册到令牌取消事件 (Token.Register
),如果令牌被取消则抛出 (Token.ThrowIfCancellationRequested
)。
要么您订阅取消事件并执行您自己的 cancel/cleanup 逻辑,要么在您的函数代码中检查是否应该取消您的操作。
示例如下所示:
private static async Task CreateTask2(CancellationToken token)
{
try
{
// Pass on the token when calling other functions.
await Task.Delay(8000, token);
// And manually check during long operations.
for (int i = 0; i < 10000; i++)
{
// Do we need to cancel?
token.ThrowIfCancellationRequested();
// Simulating work.
Thread.SpinWait(5000);
}
Console.WriteLine("This is task two");
}
catch (Exception e)
{
Console.WriteLine("Task 2 was cancelled by Task 1");
Console.WriteLine(e);
}
}
通过 Register
注册委托只是一种通知令牌何时进入取消状态的方式,仅此而已。为了进行取消,您需要在代码中对此通知做出反应,并且当您要取消的执行进入未验证取消令牌的阶段时(例如,因为正在执行的方法不接受) CancellationToken
作为参数),但您仍然需要对取消状态进行一些控制。但是在所有情况下,当您处理可以访问 CancellationToken
的代码的执行时,您不需要订阅取消通知。
在你的例子中,第一个委托引发了异常,这个异常被传播到 Cancel
调用,这就是任务被取消的原因,但这是不正确的设计,因为你不应该处理 CancellationTokenSource
在你的任务中,不应该在那里发起取消,所以我想说第一次取消只是巧合。对于第二个任务,委托被调用,但没有触发任务内部的取消,所以为什么要取消它?
第一件事是,当您调用 CancellationToken.Register
时,它通常所做的只是存储委托以供稍后调用。
thread/logic 流程调用 CancellationTokenSource.Cancel
运行 所有以前注册的代表,无论这些代表是从哪里注册的。这意味着那些通常抛出的任何异常与调用 Register.
旁注 1: 我在上面说 通常 ,因为有一种情况 Register
的调用将 运行代表马上。我认为这就是 msdn 文档特别混乱的原因。具体来说:如果令牌已经被取消,那么 Register
将立即 运行 委托,而不是稍后将其存储为 运行。在下面发生在 CancellationTokenSource.InternalRegister
.
完成图片的第二件事是 CancellationToken.ThrowIfCancellationRequested
所做的就是在 运行 来自的任何地方抛出异常。那通常是 CancellationTokenSource.Cancel
被调用的地方。请注意,通常所有已注册的代表都是 运行,即使其中一些会引发异常。
旁注 2: 抛出 ThreadAbortException
会更改 Cancel
方法中的预期逻辑,因为无法捕获该特殊异常。面对这种情况时,取消会停止 运行ning 任何进一步的代表。即使在捕获异常时,调用代码也会发生同样的情况。
最后要注意的是,CancellationToken 的存在不会影响方法的逻辑流。方法中的所有行 运行,除非有代码明确退出该方法,例如,通过抛出异常。如果您将取消令牌传递给 Task.Delay 调用并且它在时间过去之前从其他地方被取消,就会发生这种情况。如果您在方法中的特定行之后调用 CancellationToken.ThrowIfCancellationRequested
,也会发生这种情况。