原创

Python Condition 被唤醒以后仍要检查条件:写一个可关闭的有界缓冲区

消费者从Condition.wait返回,不代表队列里已经有数据。它只意味着等待过程结束、重新拿到了锁;在这之前,其他消费者可能先取走数据,通知方也可能根本没有增加数据。

正确的等待条件必须由共享状态决定,在持锁时反复检查。if not items: wait()把一次醒来当成数据许可;while not items: wait()或wait_for则把醒来当成重新判断的机会。区别不在循环写法漂亮,而在锁释放期间状态可能已经变化。

下面使用Python 3.12,先做一次明确的“只有通知、没有数据”,再写一个支持关闭的有界缓冲区。关闭的语义也要事先确定:不再接收新条目,已有条目允许排空,最后让消费者退出。

只有通知的那次等待,不能返回业务结果

保存condition_buffer.py。开头的小实验不用随机睡眠安排谁先执行,用Event确认消费者已经进入持锁等待区域。

from collections import deque
from threading import Condition, Event, Thread


def notification_is_not_data():
    condition = Condition()
    values = deque()
    waiting = Event()
    completed = Event()
    result = []
    def consumer():
        with condition:
            waiting.set()
            condition.wait_for(lambda: bool(values))
            result.append(values.popleft())
        completed.set()
    worker = Thread(target=consumer)
    worker.start()
    try:
        if not waiting.wait(2):
            raise RuntimeError('consumer did not start')
        with condition:
            condition.notify_all()  # Deliberately does not change the predicate.
        assert not completed.wait(0.05), 'notification must not fabricate data'
    finally:
        with condition:
            values.append('real item')
            condition.notify_all()
        worker.join(2)
    if worker.is_alive():
        raise RuntimeError('consumer failed to finish')
    assert result == ['real item']
    print('notification alone did not complete; real item delivered')


class ClosedBuffer(Exception):
    pass


class Buffer:
    def __init__(self, capacity):
        if type(capacity) is not int or capacity < 1:
            raise ValueError('positive integer capacity required')
        self._capacity = capacity
        self._items = deque()
        self._closed = False
        self._condition = Condition()

    def put(self, item):
        with self._condition:
            self._condition.wait_for(
                lambda: self._closed or len(self._items) < self._capacity
            )
            if self._closed:
                raise ClosedBuffer('buffer is closed for producers')
            self._items.append(item)
            self._condition.notify_all()

    def get(self):
        with self._condition:
            self._condition.wait_for(lambda: self._items or self._closed)
            if not self._items:
                raise ClosedBuffer('buffer is drained')
            item = self._items.popleft()
            self._condition.notify_all()
            return item

    def close(self):
        with self._condition:
            self._closed = True
            self._condition.notify_all()


def main():
    notification_is_not_data()
    buffer = Buffer(2)
    received = []
    def consume():
        while True:
            try:
                item = buffer.get()
            except ClosedBuffer:
                return
            received.append(item)
    worker = Thread(target=consume)
    worker.start()
    try:
        for item in range(5):
            buffer.put(item)
    finally:
        buffer.close()
    worker.join(2)
    if worker.is_alive():
        raise RuntimeError('consumer did not stop after draining')
    assert received == list(range(5))
    try:
        buffer.put(99)
        raise AssertionError('closed producer should be rejected')
    except ClosedBuffer:
        print('put after close: rejected')
    print('drained:', received)

if __name__ == '__main__':
    main()

执行 python3 condition_buffer.py。通知实验先确认没有业务结果,再追加真实条目,消费者才完成;缓冲区最终按顺序交付0到4,关闭后写入被拒绝。

0.05秒只用于确认受控失败路径没有提前完成,不是线程唤醒延迟基准。程序中的两秒join同样是实验故障探测,不能解释成Buffer每次put/get都支持两秒超时。方法本身可以一直等待,取消与时间预算若属于接口,应另外设计。

状态检查与修改必须共用同一把锁

容量检查如果放在with外面,两个生产者可能都观察到还有位置,随后各自入队,超过上限。Condition帮助等待,不会替分散在不同锁中的状态动作建立原子性。

本例把谓词、入队、出队和关闭全部放在同一Condition锁内。wait_for等待时释放锁,返回前重新取得锁,谓词也在这个同步边界内判断。调用方拿到item以后才做业务处理,避免把慢工作放在共享锁里。

notify并不释放当前持有的锁。通知方在with内完成状态更新,等它退出,等待方才有机会重新获得锁。若在持锁期间执行远程调用,即使已经notify,其他线程也可能继续堵在锁上。

关闭必须唤醒两种等待者

消费者可能在等非空,生产者可能在等非满。close把_closed设为True,并notify_all,让两类等待都能重新判断退出条件。只唤醒消费者,满队列里的生产者仍可能永远等不到可以写入的状态。

消费者检查“有条目就先取”,因此关闭后可以排空。生产者则在唤醒后先检查关闭,哪怕此时刚好有空位,也不再接受新输入。这两条顺序共同表达先前声明的结束合同。

若业务要求关闭后立即丢弃剩余项,应明确清空与资源回收规则,并调整测试。不要把当前排空实现的某一行顺手改掉,却仍沿用“全部条目都处理完成”的日志。

以后增加超时参数时,也要处理wait_for返回假值的路径。等待预算到期不代表谓词成立,不能随后照常popleft或append;应返回明确超时,并保持共享状态未被错误修改。截止时间最好覆盖整个等待过程,而不是每次被唤醒以后重新获得一份完整预算。

有些接口还要区分“等待超时”与“缓冲区已经关闭”。二者都可能结束等待,却对应不同调用方动作:超时可能继续等待或放弃,关闭则表示这份输入生命周期已经结束。把它们统一返回None,还会和本来允许入队的None载荷发生混淆,所以示例用单独异常表达关闭状态。

notify_all 简单,但不承诺公平

一个Condition同时服务非空和非满两个谓词,notify_all让所有等待者重新竞争,逻辑容易核对。数据量与等待者很多时,频繁全体唤醒可能增加竞争;标准Queue等成熟工具已有更完整的并发协议,普通应用不必为这份演示重写基础容器。

通知也不指定下一条由哪个消费者领取,不承诺先进等待者先获得锁。FIFO条目顺序与线程调度公平性不是同一保证。需要按租户轮转或任务优先级处理时,应在调度模型里表达,不能期待Condition自动提供。

这里没有任务完成计数,get成功只表示领取,不表示业务持久成功。处理失败的重试、确认和死信另有协议;不要因为消费者最终排空队列,就把整个批次标记为业务成功。

演示之外,异常退出也要有结束路径

main用finally关闭缓冲区,避免生产循环出错后让消费者永远等新数据。如果消费者可能自己抛业务异常,真实程序还应将失败传回协调者,关闭输入并等待其他线程结束,不能让生产者继续堵在已无消费者的满队列上。

因此这份Buffer是同步机制实验,正常路径把领取结果收集到列表,故意没有模拟外部业务失败。加入外部处理以后,任务所有者需要建立错误传播和收尾;线程函数抛异常不会自动把异常抛回启动它的main。

重新检查一个Condition等待点时,可以直接问三件事:当前谓词是什么,谁在同一锁里改变它,关闭或失败时谁负责唤醒。这三项都明确,wait返回以后才不会被误当成一张不需要再次验证的通行证。

正文到此结束
评论插件初始化中...
Loading...