由于不必要的性能影响,我的问题具体指的是为什么它是这样设计的。
当线程 T1 有这段代码时:
cv.acquire()
cv.wait()
cv.release()
和线程 T2 有这个代码:
cv.acquire()
cv.notify() # requires that lock be held
cv.release()
发生的事情是 T1 等待并释放锁,然后 T2 获取它,通知 cv
唤醒 T1。现在,在从 wait()
返回后,T2 的释放与 T1 的重新获取之间存在竞争条件。如果 T1 尝试首先重新获取,它将被不必要地重新挂起,直到 T2 的 release()
完成。
注意: 我有意不使用 with
语句,以更好地说明显式调用的竞争。
这似乎是一个设计缺陷。是否有任何已知的理由,或者我错过了什么?
原文由 Yam Marcovic 发布,翻译遵循 CC BY-SA 4.0 许可协议
这不是一个明确的答案,但它应该涵盖我设法收集到的关于这个问题的相关细节。
首先,Python 的 线程实现是基于 Java 的. Java 的
Condition.signal()
文档如下:现在,问题是为什么要特别在 Python 中 强制执行 此行为。但首先我想介绍一下每种方法的优缺点。
至于为什么有些人认为持有锁通常是更好的主意,我发现了两个主要论点:
从服务员
acquire()
锁定的那一刻起——也就是说,在wait()
上释放它之前——它保证会收到信号通知。如果相应的release()
发生在信号之前,这将允许序列(其中 P=Producer 和 C=Consumer )P: release(); C: acquire(); P: notify(); C: wait()
在这种情况下wait()
对应同一流的acquire()
会错过信号。在某些情况下这无关紧要(甚至可以被认为更准确),但在某些情况下这是不可取的。这是一个论点。当你
notify()
在锁外,这可能会导致调度优先级倒置;也就是说,低优先级线程可能最终会比高优先级线程获得优先级。考虑一个具有一个生产者和两个消费者( LC=低优先级消费者 和 _HC=高优先级消费者_)的工作队列,其中 LC 当前正在执行一个工作项,而 HC 在wait()
中被阻塞。可能会出现以下顺序:
而如果
notify()
发生在release()
之前, LC 将无法acquire()
在 HC 被唤醒之前。这就是发生优先级反转的地方。这是第二个说法。支持在锁外通知的论点是为了高性能线程,线程不需要回到睡眠状态只是为了在它获得的下一个时间片再次唤醒——这已经解释了它是如何发生的我的问题。
Python的
threading
模块在 Python 中,正如我所说,您必须在通知时持有锁。具有讽刺意味的是,内部实现不允许底层操作系统避免优先级反转,因为它对等待者强制执行 FIFO 顺序。当然,服务员的顺序是确定性的这一事实可能会派上用场,但问题仍然是为什么要强制执行这样的事情,因为有人认为区分锁和条件变量会更精确,因为在一些需要优化并发和最小阻塞的流,
acquire()
不应该自己注册一个前面的等待状态,而应该只有wait()
调用自己。可以说,Python 程序员无论如何都不会关心性能到这种程度——尽管这仍然没有回答为什么在实现标准库时不应该允许多个标准行为成为可能的问题。
还有一件事要说的是
threading
模块的开发人员可能出于某种原因特别想要一个 FIFO 顺序,并且发现这是实现它的最佳方式,并且想要建立它作为Condition
以其他(可能更普遍的)方法为代价。为此,在他们自己解释之前,他们应该从怀疑中获益。