第 9 个线程上的 SemaphoreSlim 死锁

Posted

技术标签:

【中文标题】第 9 个线程上的 SemaphoreSlim 死锁【英文标题】:SemaphoreSlim deadlocks on 9th thread 【发布时间】:2021-12-06 16:21:57 【问题描述】:

我有一个带有多线程测试的测试自动化环境,它使用共享的HttpClient 来测试我们 Web API 上的方法。在HttpClient 被初始化之后,它可以被我们在多个线程上运行的所有测试使用,因为它是一个线程安全的对象。然而,防止初始化不止一次发生是一个挑战。此外,它包含了 await 关键字,因此它不能使用任何基本的锁技术来确保初始化操作是原子的。

为了确保初始化正确进行,我使用SemaphoreSlim 来创建一个用于初始化的互斥锁。要访问该对象,所有测试都必须调用一个使用 SemaphoreSlim 的函数,以确保它已被第一个请求它的线程正确初始化。

我找到了在this web page 上使用SemaphoreSlim 的以下实现。

public class TimedLock

    private readonly SemaphoreSlim toLock;

    public TimedLock()
    
        toLock = new SemaphoreSlim(1, 1);
    

    public LockReleaser Lock(TimeSpan timeout)
    
        if (toLock.Wait(timeout))
        
            return new LockReleaser(toLock);
        

        throw new TimeoutException();
    

    public struct LockReleaser : IDisposable
    
        private readonly SemaphoreSlim toRelease;

        public LockReleaser(SemaphoreSlim toRelease)
        
            this.toRelease = toRelease;
        
        public void Dispose()
        
            toRelease.Release();
        
    

我是这样使用这个类的:

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

protected async Task<HttpClient> GetClient()

    using (_timedLock.Lock(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;
    

直到最近,当我在我的代码中添加了第九个线程时,它才能完美运行。我不得不将其拨回 8 个线程,因为每当我允许第 9 个线程调用 TimedLock.Lock 方法时,整个程序就会死锁。

有谁知道可能发生了什么,或者如何解决这个问题?

【问题讨论】:

会不会和出站连接限制有关? ***.com/questions/31735569/… 尝试提高到 10 以上。 IMO 这一切都太过分了。使用Interlocked.Exchange 并在返回值不为空时处理它 您能告诉我们您如何使用SemaphoreSlim 来保护HttpClient 对象的初始化吗?如果您在没有linked TimedLock 类的帮助下使用SemaphoreSlim,您是否也测试过您的程序仍然死锁?此外,作为旁注,Lazy&lt;HttpClient&gt; 实例看起来比使用SemaphoreSlim+code-found-on-the-web 更简单地解决此问题。 @TheodorZoulias 很好的问题。虽然我之前没有见过 Lazy ,但我怀疑它的问题将归结为初始化代码本身在其中等待的事实,因此不能保证同一个线程在启动后完成初始化。我将更新问题以至少包含说明该问题的伪代码。 约翰using (_timedLock.Lock(... 是一个阻塞调用,因为TimedLock 调用Wait 方法而不是正确的WaitAsync。这可能导致ThreadPool 饥饿。如果您的机器有 8 个内核,那么 ThreadPool 以按需提供的 8 个线程开始。这条线也很可疑:Response = await result...Response 是什么?这是否意味着GetClient() 有副作用?最后,异步初始化可以看this问题。 【参考方案1】:

好的。我发现了自己的问题,这真的是我自己的问题,不是别人的。

如果您将我上面的代码与我引用的源代码进行比较,您会发现实际上存在差异。原代码使用 SemaphoreSlim 内置的 WaitAsync 函数异步实现 Lock 函数:

public async Task<LockReleaser> Lock(TimeSpan timeout)

    if(await toLock.WaitAsync(timeout))
    
        return new LockReleaser(toLock);
    
    throw new TimeoutException();

当然,在我使用它的代码中,在适当的位置添加 await 关键字以正确处理添加的 Task 对象:

...
using (await _timedLock.Lock(TimeSpan.FromSeconds(6000)))

...

昨天我“发现”如果我将 toLock 对象更改为使用 WaitAsync,问题就神奇地消失了,我为自己感到非常自豪。但就在几分钟前,当我将“原始”代码复制并粘贴到我的问题中时,我意识到“原始”代码实际上包含“我的”修复!

我现在记得几个月前看过这个并想知道为什么他们需要将其设为异步函数。因此,以我的高超智慧,我在没有 Async 的情况下尝试了它,发现它运行良好,然后继续,直到我最近才开始使用足够多的线程来证明为什么它是必要的!

所以为了不让人们感到困惑,我将问题中的代码更改为我最初更改的错误代码,并将上面真正原始的好代码放在“我的”答案中,这实际上应该归功于致引用文章的作者 Richard Blewett。

我不能说我完全理解为什么这个修复确实有效,所以任何可以更好地解释它的进一步答案都非常受欢迎!

【讨论】:

没有看到更多答案,所以我将其标记为答案,至少现在是这样。

以上是关于第 9 个线程上的 SemaphoreSlim 死锁的主要内容,如果未能解决你的问题,请参考以下文章

跳过 SemaphoreSlim 而不是等待

线程编程-使用SemaphoreSlim类

多线程10-SemaphoreSlim

多线程-3(同步)

一文看懂 Mutex vs Semaphore vs Monitor vs SemaphoreSlim

增加/减少 SemaphoreSlim 中可用插槽的数量