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

SemaphoreSlim实现HttpClient异步初始化时第9线程死锁问题咨询

问题成因

你遇到的死锁核心是两个问题叠加导致的:

  1. 你使用的TimedLock中调用的是SemaphoreSlim.Wait()同步阻塞方法,调用该方法的线程会被完全阻塞挂起,无法释放给其他任务使用。
  2. .NET 默认线程池的最小工作线程数等于设备的CPU逻辑核心数,大多数消费级设备的默认值为8。当你启动第9个线程时,所有8个线程池线程都被阻塞在Wait()调用上,等待锁释放;而当前持有锁的线程在执行await操作后,需要空闲的线程池线程才能继续执行剩余逻辑、释放锁,两方互相等待就形成了死锁。

解决方案

最优方案:改用异步锁实现

把TimedLock的同步等待逻辑改为异步等待,使用SemaphoreSlim.WaitAsync()替代同步Wait(),等待锁的线程不会被阻塞,会释放回线程池供其他任务使用,从根源避免线程池被占满的问题。

修改后的异步版TimedLock实现:

public class TimedLock
{
    private readonly SemaphoreSlim _toLock = new SemaphoreSlim(1, 1);

    public async Task<LockReleaser> LockAsync(TimeSpan timeout)
    {
        if (!await _toLock.WaitAsync(timeout))
        {
            throw new TimeoutException();
        }
        return new LockReleaser(_toLock);
    }

    public struct LockReleaser : IDisposable
    {
        private readonly SemaphoreSlim _toRelease;
        public LockReleaser(SemaphoreSlim toRelease) => _toRelease = toRelease;
        public void Dispose() => _toRelease.Release();
    }
}

同时修改GetClient方法,新增双重检查减少不必要的锁竞争,调用异步版的LockAsync:

private static HttpClient _client;
private static TimedLock _timedLock = new();

protected async Task<HttpClient> GetClient()
{
    // 第一重检查:初始化完成后直接返回,无需进入锁逻辑
    if (_client != null)
    {
        return _client;
    }
    using (await _timedLock.LockAsync(TimeSpan.FromSeconds(600)))
    {
        // 第二重检查:避免多个等待锁的线程重复执行初始化
        if (_client != null)
        {
            return _client;
        }

        MyWebApplicationFactory<Startup> factory = new();
        _client = factory.CreateClient();

        Request myRequest = new Request()
        {
            //.....Authentication code
        };

        HttpResponseMessage result = await _client.PostAsJsonAsync("api/accounts/Authenticate", myRequest);
        result.EnsureSuccessStatusCode();
        AuthenticateResponse Response = await result.Content.ReadAsAsync<AuthenticateResponse>();

        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Response.Token);
        return _client;
    }
}

简化方案:使用AsyncLazy替代自定义锁

如果你可以引入Microsoft.VisualStudio.Threading包,直接使用内置的AsyncLazy就能实现异步安全的单例初始化,不需要自己维护锁逻辑:

private static readonly AsyncLazy<HttpClient> _client = new(async () =>
{
    MyWebApplicationFactory<Startup> factory = new();
    var client = factory.CreateClient();

    Request myRequest = new Request()
    {
        //.....Authentication code
    };

    HttpResponseMessage result = await client.PostAsJsonAsync("api/accounts/Authenticate", myRequest);
    result.EnsureSuccessStatusCode();
    AuthenticateResponse Response = await result.Content.ReadAsAsync<AuthenticateResponse>();

    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Response.Token);
    return client;
});

// 直接调用即可,内置线程安全和初始化逻辑
protected Task<HttpClient> GetClient() => _client.GetValueAsync();

临时补救方案:调高线程池最小线程数

如果暂时无法修改锁实现,可以在程序启动入口处调高线程池最小工作线程数,避免线程池被快速占满:

// 比如设置最小工作线程数为32,IO线程数为32
ThreadPool.SetMinThreads(32, 32);

该方案只能暂时缓解问题,无法从根源避免死锁,优先推荐使用异步锁方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.29 22:57:01