首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >如果信号量立即释放,取消SemaphoreSlim.WaitAsync(CancellationToken)并不总是引发OperationCanceledException

如果信号量立即释放,取消SemaphoreSlim.WaitAsync(CancellationToken)并不总是引发OperationCanceledException
EN

Stack Overflow用户
提问于 2022-07-14 15:59:16
回答 2查看 178关注 0票数 4

考虑以下.Net 6控制台程序:

代码语言:javascript
运行
复制
// 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个任务的报告按预期取消。

我在某个地方假设一个种族状况,但我的问题是:我是否错误地期望我的既定行为,如果是的话,为什么?

非常感谢。

示例输出:

代码语言:javascript
运行
复制
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
EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2022-07-14 16:49:26

CancellationToken使用了一个“协作”的取消模型,这意味着它是非阻塞的,并且依赖于令牌的使用者来取消,而不是在每个任务绑定方法中都这样做。

因此,如果对取消请求的响应出现延迟,则可以体验您所描述的竞争条件的类型。您是通过在确保完成.Release()调用之前调用Task.WhenAll()创建的,这意味着有可能发生以下情况:

  • 要求取消
  • 有一项任务成功地完成了semaphore.WaitAsync(),但随后被Task.Delay搁置
  • 释放信号量称为
  • 大多数任务取消(那些通过semaphore.WaitAsync()没有成功完成输入的任务)。

这样做的唯一原因首先是因为在调用发布之前添加了人为的延迟。删除await Task.Delay(500)将导致异常。

如果您想用现有的方法来避免这种行为,您可以将呼叫顺序更改为:

代码语言:javascript
运行
复制
cts.Cancel();
await t;
semaphore.Release();

这将阻止信号量在所有任务完成之前释放,从而允许每个任务协同取消,即使一个任务的工作仍将完成。它提供了以下输出:

代码语言:javascript
运行
复制
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

最后,请注意,在现实世界中,您不应该编写这样的代码。每一个完成工作的任务都应该在信号量完成后释放出来,以避免您所创建的无数竞争条件。

票数 3
EN

Stack Overflow用户

发布于 2022-07-14 16:47:57

首先也是最重要的,通常取消并不保证抛出异常,只是Task在不久的将来终止,这可能会通过返回短路。

应该始终使用信号量,类似于此或同步等效:

代码语言:javascript
运行
复制
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 手动

代码语言:javascript
运行
复制
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的副本。如果异步方法是无状态的,那么您的生活会更容易一些。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/72983452

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档