考虑以下.Net 6控制台程序:
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
var semaphore = new SemaphoreSlim(1, 1);
var cts = new CancellationTokenSource();
var tasks = Enumerable.Range(0, 10).Select(WaitingTask);
var t = Task.Run(async () =>
{
await Task.WhenAll(tasks);
Console.WriteLine("Tasks complete");
});
await Task.Delay(500);
Console.WriteLine("Press any key to Cancel waiting tasks, then immediately release semaphore");
Console.ReadKey();
cts.Cancel();
semaphore.Release();
await t;
async Task WaitingTask(int i)
{
try
{
Console.WriteLine($"{i} Waiting");
await semaphore.WaitAsync(cts.Token);
Console.WriteLine($"{i} Aquired");
await Task.Delay(50);
Console.WriteLine($"{i} Complete");
}
catch (OperationCanceledException)
{
Console.WriteLine($"{i} Cancelled");
}
}
它创建了10个任务,这些任务试图获取信号量上的锁,每次只允许一个条目。在第一个任务报告完成后,而其他9个任务报告它们正在等待信号量之后,我希望取消传递给等待任务的令牌,然后立即释放信号量上的锁。
期望:其余9个任务抛出并处理OperationCanceledException,并报告“已取消”。
实际:其余8项任务将完成,但其中1项任务将成功地进入信号量并正常完成。也就是说,您无法可靠地取消对WaitAsync(CancellationToken)
的调用
注释掉行semaphore.Release();
将导致所有9个任务的报告按预期取消。
我在某个地方假设一个种族状况,但我的问题是:我是否错误地期望我的既定行为,如果是的话,为什么?
非常感谢。
示例输出:
Hello, World!
0 Waiting
0 Aquired
1 Waiting
2 Waiting
3 Waiting
4 Waiting
5 Waiting
6 Waiting
7 Waiting
8 Waiting
9 Waiting
0 Complete
Press any key to Cancel waiting tasks, then immediately release semaphore
1 Aquired
4 Cancelled
8 Cancelled
5 Cancelled
9 Cancelled
6 Cancelled
3 Cancelled
2 Cancelled
7 Cancelled
1 Complete
Tasks complete
发布于 2022-07-14 16:49:26
CancellationToken
使用了一个“协作”的取消模型,这意味着它是非阻塞的,并且依赖于令牌的使用者来取消,而不是在每个任务绑定方法中都这样做。
因此,如果对取消请求的响应出现延迟,则可以体验您所描述的竞争条件的类型。您是通过在确保完成.Release()
调用之前调用Task.WhenAll()
创建的,这意味着有可能发生以下情况:
semaphore.WaitAsync()
,但随后被Task.Delay搁置semaphore.WaitAsync()
没有成功完成输入的任务)。这样做的唯一原因首先是因为在调用发布之前添加了人为的延迟。删除await Task.Delay(500)
将导致异常。
如果您想用现有的方法来避免这种行为,您可以将呼叫顺序更改为:
cts.Cancel();
await t;
semaphore.Release();
这将阻止信号量在所有任务完成之前释放,从而允许每个任务协同取消,即使一个任务的工作仍将完成。它提供了以下输出:
Hello, World!
0 Waiting
0 Aquired
1 Waiting
2 Waiting
3 Waiting
4 Waiting
5 Waiting
6 Waiting
7 Waiting
8 Waiting
9 Waiting
0 Complete
Press any key to Cancel waiting tasks, then immediately release semaphore
9 Cancelled
3 Cancelled
1 Cancelled
7 Cancelled
8 Cancelled
4 Cancelled
5 Cancelled
6 Cancelled
2 Cancelled
Tasks complete
最后,请注意,在现实世界中,您不应该编写这样的代码。每一个完成工作的任务都应该在信号量完成后释放出来,以避免您所创建的无数竞争条件。
发布于 2022-07-14 16:47:57
首先也是最重要的,通常取消并不保证抛出异常,只是Task
在不久的将来终止,这可能会通过返回短路。
应该始终使用信号量,类似于此或同步等效:
await sem.WaitAsync(ct)
try {
[...]
} finally {
sem.Release();
}
永远不要释放一个未获得的信号量。
无论如何,看看你在sharplab.io中的片段,很明显,在取消之前拖延很长时间才能解决这个问题,我们得到了一个竞赛条件。CLR可以从技术上重新排序语句(所有认为的异步方法一般都不会重新排序),这样cts.Cancel();
就会发生在semaphore.Release()
之后,而不是案例。
让我们看一看SemaphoreSlim
源代码,特别是SemaphoreSlim.WaitAsync(CancellationToken)
,它在锁链接列表中排队等待任务,扩展Task
。这意味着TAP处理取消。在大多数情况下,例如任务,cts由使用者定期检查,因此无法保证立即取消。当从另一个线程取消时,信号量显式地启用始终抛出OperationCanceledException
。
我对为什么await Task.Delay(10);
终止正确的最好猜测是,当等待Task
时,TAP会验证一些/全部cts。这就是为什么信号量的任务队列取消的原因。
到解决方案上。如果您想要精确的时间,应该使用Monitor
锁来验证状态,或者在您的情况下更好地检查CancellationToken
手动
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
var semaphore = new SemaphoreSlim(1, 1);
var cts = new CancellationTokenSource();
var tasks = Enumerable.Range(0, 10).Select( i => WaitingTask(i, cts.Token));
var t = Task.Run(async () =>
{
await Task.WhenAll(tasks);
Console.WriteLine($"Tasks complete {Thread.CurrentThread.ManagedThreadId}");
});
await Task.Delay(500);
Console.WriteLine($"Cancelling {Thread.CurrentThread.ManagedThreadId}");
cts.Cancel();
//await Task.Delay(10); // comment out and see
semaphore.Release();
await t;
static async Task WaitingTask(int i, CancellationToken ct)
{
int tid = Thread.CurrentThread.ManagedThreadId;
try
{
Console.WriteLine($"{i} Waiting {tid}");
await semaphore.WaitAsync(ct);
ct.ThrowIfCancellationRequested();
Console.WriteLine($"{i} Aquired {tid}");
await Task.Delay(50);
Console.WriteLine($"{i} Complete {tid}");
}
catch (OperationCanceledException)
{
Console.WriteLine($"{i} Cancelled {tid}");
}
}
请注意,您不应该在闭包中使用cts,而应该获取cts.Token: CancellationToken
的副本。如果异步方法是无状态的,那么您的生活会更容易一些。
https://stackoverflow.com/questions/72983452
复制相似问题