混用SemaphoreSlim的Wait与WaitAsync导致死锁问题排查
SemaphoreSlim混用Wait/WaitAsync导致死锁问题分析
问题场景:Adder类代码
class Adder { private readonly List<int> _content = []; private readonly SemaphoreSlim _semaphoreSlim = new(1); public int Total { get { _semaphoreSlim.Wait(); try { return _content.Sum(); } finally { _semaphoreSlim.Release(); } } } public async Task Add(int numberToAdd) { await _semaphoreSlim.WaitAsync().ConfigureAwait(false); try { await Task.Delay(10).ConfigureAwait(false); _content.Add(numberToAdd); } finally { _semaphoreSlim.Release(); } } }
死锁成因推理
- 调用者调用
Add方法 - 成功获取信号量进入临界区
Task.Delay释放当前线程,允许调度新任务- 同一调用者触发的新任务(并行执行)请求
Total属性 - 新任务调用
Wait()阻塞线程,等待信号量许可,但原Add任务因线程被阻塞无法完成释放,最终导致死锁
复现尝试(未成功)
using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; internal static class Program { public static async Task Main() { Console.WriteLine("Start..."); const int numberOfTasks = 10; const int numberOfIterations = 1_000; var adder = new Adder(); var tasks = new List<Task>(); for (var i = 0; i < numberOfTasks; i++) { tasks.Add(Iterate(i, numberOfIterations, adder)); } await Task.WhenAll(tasks).ConfigureAwait(false); Console.WriteLine("Done!"); } private static async Task Iterate(int identifier, int numberOfIterations, Adder adder) { Console.WriteLine($"{Environment.CurrentManagedThreadId} {Thread.CurrentThread.Name} {identifier} Starting..."); await Task.Delay(1).ConfigureAwait(false); for (var j = 0; j < numberOfIterations; j++) { await Task.Delay(1).ConfigureAwait(false); var total = adder.Total; await Task.Delay(1).ConfigureAwait(false); if (total % 2 == 0) { await adder.Add(3).ConfigureAwait(false); } else { await adder.Add(2).ConfigureAwait(false); } Console.WriteLine($"{Environment.CurrentManagedThreadId} {identifier} {j}"); } Console.WriteLine($"{Environment.CurrentManagedThreadId} {identifier} Done"); } }
技术疑问与解答
1. 上述死锁推理是否正确?
推理方向正确,但核心细节需要修正:
SemaphoreSlim是线程无关的信号量,它不绑定具体线程,只记录已获取的许可数。真正的死锁逻辑是:
Add方法获取信号量后,await Task.Delay会释放当前线程回线程池;- 若此时有任务在单线程同步上下文(如WPF/WinForms主线程、老ASP.NET请求上下文)中调用
Total的Wait(),该线程会被阻塞,等待信号量许可; Add方法在Delay完成后,需要回到原同步上下文执行后续代码并释放信号量,但此时同步上下文线程已被Wait()阻塞,导致Add永远无法释放信号量,Total的Wait()也永远无法获取许可,最终形成死锁。
如果代码运行在无同步上下文的环境(如控制台程序),死锁概率极低,因为线程池可以调度其他线程执行Add的后续代码。
2. 同时使用Wait和WaitAsync是否属于设计缺陷?
是的,这属于明确的设计缺陷,原因有两点:
- 死锁风险:同步阻塞的
Wait()会占用线程,而异步的WaitAsync()会释放线程。两者混用后,极易出现异步任务需要线程释放资源,但线程被同步阻塞占用的情况,尤其在有同步上下文的环境中; - API一致性问题:同一资源的访问接口应统一同步或异步模式。
Total属性的同步阻塞设计,与异步的Add方法矛盾,破坏了异步流程的非阻塞特性,调用者无法在异步代码中优雅地获取Total值。
3. 如何复现该死锁问题?
要复现,必须模拟单线程同步上下文环境,让异步任务的后续代码和同步阻塞调用抢占同一线程。以下是可复现的代码示例:
using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; // 手动模拟单线程同步上下文 public class SingleThreadSynchronizationContext : SynchronizationContext { private readonly Queue<(SendOrPostCallback, object)> _queue = new(); private readonly Thread _thread; private bool _isRunning; public SingleThreadSynchronizationContext() { _thread = new Thread(RunLoop); _thread.IsBackground = true; _thread.Start(); } private void RunLoop() { _isRunning = true; SetSynchronizationContext(this); while (_isRunning) { (SendOrPostCallback callback, object state) = default; lock (_queue) { if (_queue.Count > 0) { callback = _queue.Dequeue(); } } callback?.Invoke(state); Thread.Yield(); } } public override void Post(SendOrPostCallback d, object state) { lock (_queue) { _queue.Enqueue((d, state)); } } public override void Send(SendOrPostCallback d, object state) { throw new NotImplementedException(); } } internal static class Program { public static async Task Main() { Console.WriteLine("Start..."); // 切换到单线程同步上下文 var syncContext = new SingleThreadSynchronizationContext(); SynchronizationContext.SetSynchronizationContext(syncContext); var adder = new Adder(); // 先触发Add方法,获取信号量后进入await延迟 var addTask = adder.Add(1); // 在同步上下文中触发Total属性的同步调用,阻塞线程 var totalTask = Task.Run(() => { syncContext.Post(_ => { Console.WriteLine("Trying to get Total..."); var total = adder.Total; Console.WriteLine($"Total: {total}"); }, null); }); try { await Task.WhenAll(addTask, totalTask).ConfigureAwait(false); } catch (Exception ex) { Console.WriteLine($"Exception: {ex.Message}"); } Console.WriteLine("Done!"); } } class Adder { private readonly List<int> _content = []; private readonly SemaphoreSlim _semaphoreSlim = new(1); public int Total { get { Console.WriteLine($"Total getter waiting on thread {Environment.CurrentManagedThreadId}"); _semaphoreSlim.Wait(); try { return _content.Sum(); } finally { _semaphoreSlim.Release(); } } } public async Task Add(int numberToAdd) { Console.WriteLine($"Add waiting on thread {Environment.CurrentManagedThreadId}"); await _semaphoreSlim.WaitAsync().ConfigureAwait(true); // 强制回到原同步上下文 try { Console.WriteLine($"Add acquired semaphore on thread {Environment.CurrentManagedThreadId}"); await Task.Delay(100); // 不使用ConfigureAwait(false),确保后续代码回到同步上下文 _content.Add(numberToAdd); Console.WriteLine($"Add completed on thread {Environment.CurrentManagedThreadId}"); } finally { _semaphoreSlim.Release(); } } }
运行这段代码后,你会看到程序卡在Trying to get Total...和Add acquired semaphore on thread X的输出,无法继续执行,即触发了死锁。
内容的提问来源于stack exchange,提问作者David van Luijk
相关产品推荐
相关产品推荐

