SemaphoreSlim异常:是自身Bug还是自定义SynchronizationContext导致?
我们做了个类似Reactive EventLoopScheduler的线程抽象:
- 能把任务提交到单个后台线程执行
- 如果当前线程就是目标线程,会直接同步执行任务
- 同时实现了
SynchronizationContext,确保异步回调都在这个专用线程上完成
但使用SemaphoreSlim时踩了大坑:当SynchronizationContext.Post同步执行回调时,会触发Release调用重入,直接损坏信号量状态,导致后续任务永久阻塞。我们已经写出复现代码,对比了同步/异步Post的不同表现,此前找到的类似问题解决方案都是基于默认ThreadPool上下文的,不适用于当前场景。
现在核心疑问:SynchronizationContext.Post是不是应该永远异步执行?
已知信息:
- MSDN曾提及传统ASP.NET上下文支持同步Post,但该行为在.NET Core中已被移除
- 同步Post确实容易引发重入问题,我们正考虑修改实现,但需要确认这一方向的合理性
1. Post方法的原生语义
从官方设计意图来看,Post的核心定位就是异步调度回调,而Send才是负责同步调度的方法。早期传统ASP.NET的同步Post实现,属于特定场景的历史遗留,并非通用规范。.NET Core移除该行为,本质是让Post回归了原本的异步设计初衷。
2. 同步Post的重入风险不可忽视
你的场景就是典型案例:SemaphoreSlim.Release在同步Post的回调中被重入调用,会直接破坏信号量的内部计数逻辑——因为SemaphoreSlim的Release并非可重入操作(它不支持递归语义)。这种重入不仅会损坏信号量状态,还可能引发死锁、业务逻辑混乱等更复杂的并发问题。
3. 自定义上下文的合理实现方式
对于你的EventLoopScheduler风格抽象,正确的Post实现应该遵循以下原则:
- 无论当前线程是否为目标线程,始终异步将回调调度到目标线程的任务队列
- 如果需要同步执行的语义,通过单独的
Send方法实现(或在提交任务时提供显式的同步执行选项) - 目标线程的任务队列必须保证串行执行,避免回调之间的并发冲突
4. 修改实现的注意事项
如果决定调整为始终异步Post,需要留意:
- 检查现有代码中是否依赖了同步Post的行为,做好兼容性过渡
- 异步调度的性能开销极小(单个后台线程的队列调度几乎无负担),无需过度担心性能问题
- 全面验证所有依赖该上下文的异步代码,尤其是涉及锁、信号量等同步原语的场景,确保功能正常
总结
SynchronizationContext.Post应当始终遵循异步语义实现。同步Post是历史遗留的特殊场景行为,在通用的自定义上下文实现中,异步调度是更安全、更符合设计规范的选择,能从根源上避免重入导致的同步原语损坏问题。
内容的提问来源于stack exchange,提问作者Matthew Quinn

