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

同步转异步时SemaphoreSlim与lock的使用粒度及死锁问题咨询

C#异步锁迁移:SemaphoreSlim死锁解决与锁粒度选择

一、解决SemaphoreSlim替代lock的死锁问题

lock关键字具备线程重入性——同一线程可以多次获取同一个lock锁,这意味着在lock块内触发事件,回调中再次访问需要锁的资源时不会死锁。但SemaphoreSlim不支持重入,直接替换lock会导致同一线程重复调用WaitAsync时阻塞死锁。

核心解决方案:事件触发移至锁外部

把事件触发逻辑从锁的保护范围内移出,确保回调执行时当前线程已经释放了锁,避免重入冲突。同时要提前复制事件引用,防止锁外触发时事件被多线程修改。

代码对比:

原同步lock代码(无死锁)

private readonly object _lockObj = new object();
public event EventHandler JobAdded;

public void AddJob(Job job)
{
    lock (_lockObj)
    {
        _jobs.Add(job);
        // 回调访问AllJobs时可重入lock,无死锁
        JobAdded?.Invoke(this, EventArgs.Empty);
    }
}

public IReadOnlyList<Job> AllJobs
{
    get
    {
        lock (_lockObj)
        {
            return _jobs.ToList().AsReadOnly();
        }
    }
}

错误的SemaphoreSlim替换(死锁)

private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

public async Task AddJobAsync(Job job)
{
    await _semaphore.WaitAsync();
    try
    {
        _jobs.Add(job);
        // 回调调用AllJobsAsync时,同一线程再次WaitAsync会阻塞死锁
        JobAdded?.Invoke(this, EventArgs.Empty);
    }
    finally
    {
        _semaphore.Release();
    }
}

正确实现(事件移至锁外)

public async Task AddJobAsync(Job job)
{
    EventHandler handler = null;
    await _semaphore.WaitAsync();
    try
    {
        _jobs.Add(job);
        // 提前复制事件引用,避免多线程下事件被修改
        handler = JobAdded;
    }
    finally
    {
        _semaphore.Release();
    }
    // 锁外触发事件,回调获取锁时无重入冲突
    handler?.Invoke(this, EventArgs.Empty);
}

二、复杂方法的锁粒度选择与迁移模式

权衡点回顾

  • 粗粒度锁:全局加锁(比如整个类用一个锁),优点是实现简单、开销低、死锁风险小;缺点是并发能力差,多个操作会互相阻塞。
  • 细粒度锁:仅在访问受保护资源时局部加锁,优点是并发能力强;缺点是实现复杂,需要梳理资源依赖,容易因锁顺序不当引发死锁。

迁移建议模式

  1. 优先采用粗粒度锁完成初期迁移
    对于遗留代码,先以粗粒度锁替代lock,快速保证异步场景下的线程安全。不需要深入梳理复杂的资源依赖,能最大程度减少迁移中的bug,适合先确保功能正确,再做性能优化。

  2. 按需拆分细粒度锁
    当粗粒度锁成为性能瓶颈时,再逐步拆分锁。拆分的核心原则:

    • 按独立资源组拆分:将关联性强的资源操作归为同一锁保护,比如Job的添加/修改用一个锁,只读查询用另一个锁(需注意数据一致性)。
    • 严格遵循锁的获取顺序:如果需要获取多个锁,永远按固定顺序获取(比如先获取锁A,再获取锁B),避免交叉锁导致死锁。
  3. 异步场景的特殊优化
    针对高并发的只读操作,可以考虑使用支持读写分离的锁(比如AsyncReaderWriterLock),允许多个读线程同时访问,写线程独占,平衡并发效率与线程安全。但要注意这类锁的重入规则,避免误用。

  4. 封装锁逻辑减少重复代码
    把异步锁的获取和释放封装成可复用的逻辑,比如用异步using模式,减少手动编写WaitAsync/Release的重复代码,避免遗漏释放锁:

    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    
    public async ValueTask<IDisposable> AcquireLockAsync()
    {
        await _semaphore.WaitAsync();
        return new SemaphoreReleaser(_semaphore);
    }
    
    private class SemaphoreReleaser : IDisposable
    {
        private readonly SemaphoreSlim _semaphore;
        public SemaphoreReleaser(SemaphoreSlim semaphore) => _semaphore = semaphore;
        public void Dispose() => _semaphore.Release();
    }
    
    // 使用方式
    using (await AcquireLockAsync())
    {
        // 操作受保护资源
    }
    

内容的提问来源于stack exchange,提问作者bas

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 05:53:21