You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

混用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 20:06:00