SemaphoreSlim实现HttpClient异步初始化时第9线程死锁问题咨询
问题成因
你遇到的死锁核心是两个问题叠加导致的:
- 你使用的
TimedLock中调用的是SemaphoreSlim.Wait()同步阻塞方法,调用该方法的线程会被完全阻塞挂起,无法释放给其他任务使用。 - .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
相关产品推荐
相关产品推荐

