为什么AsyncLock里面的锁不会阻塞线程呢?

Why the lock inside AsyncLock does not block the thread?

我正在尝试了解 AsyncLock 的工作原理。

首先,这里有一个片段可以证明它确实有效:

var l = new AsyncLock();
var tasks = new List<Task>();
while (true)
{
    Console.ReadLine();
    var i = tasks.Count + 1;
    tasks.Add(Task.Run(async () =>
    {
        Console.WriteLine($"[{i}] Acquiring lock ...");
        using (await l.LockAsync())
        {
            Console.WriteLine($"[{i}] Lock acquired");
            await Task.Delay(-1);
        }
    }));
}

"works" 我的意思是您可以 运行 任意数量的任务(通过按 Enter)并且线程数量不会增加。如果将其替换为传统的 lock,您会看到启动了新线程,这是我们尽量避免的。

但是您在源代码中首先看到的是... lock

谁能解释一下这是如何工作的,为什么它不会阻塞,我在这里错过了什么?

lock inside AsyncLock 的发布速度非常快。每个尝试获取 AsyncLock 的任务,成功获取其内部 lock 并且实际的锁定逻辑是通过队列完成的。

通过将 LockAsync() 包装在 using 块中,当块结束时将释放锁,因为 LockAsync returns 一个一次性对象 Keyusing 块的末尾处理,处理后锁将被释放。参见 https://github.com/StephenCleary/AsyncEx/blob/master/src/Nito.AsyncEx.Coordination/AsyncLock.cs#L182-L185

Can somebody please explain me how this works, why it doesn't block, and what am I missing here?

简短的回答是lock只是一种用于保证线程安全的内部机制。 lock 永远不会以任何方式公开,并且任何线程都无法在任何实际时间内持有该锁。这种方式类似于各种并发集合内部使用的锁。

还有一种使用 lock-free 编程的替代方法,但我发现 lock-free 编程极难编写、阅读和维护。一个很好的例子(遗憾的是不在线)是 Dr. Dobb 在 90 年代末发表的一系列文章,每篇文章都试图 out-do 最后一篇用更好的 lock-free 队列实现。事实证明它们都是错误的 - 在某些情况下,这些错误需要十多年才能找到。

对于我自己的代码,我不使用 lock-free 编程,除非代码的正确性非常明显。


至于异步锁与锁的概念,我将尝试解释一下。我有一种感觉,只有在使用异步协调原语时才会有这种感觉。关于写博客 post 我想了很多,但我没有合适的词来理解它。也就是说,这里是...

异步协调原语存在于与普通协调原语完全不同的平面上。同步原语阻塞线程和信号线程。异步原语只适用于普通对象;阻塞或信号只是 "by convention".

因此,对于正常的 lock,调用代码 必须 立即获取锁。但是对于异步 "lock",尝试的锁只是一个请求,只是一个对象。调用代码甚至不需要 await 它。可以请求 多个 锁和 await 它们以及 Task.WhenAll。或者甚至将它们与其他东西结合起来;代码可以做一些疯狂的事情,比如 (a) 等待两个锁都空闲 以发送信号(如 AsyncManualResetEvent),然后取消锁请求,如果信号先进来。

从线程的角度来看,它 kinda-sorta 类似于 user-mode 线程调度。与协作式多任务处理(与抢占式相反)也有一些相似之处。但总的来说,异步原语 "lifted" 是一个不同的平面,其中一个平面仅适用于对象和代码块,而不适用于线程。