python Condition
# Python Condition 对象一个不太起眼但很有用的同步工具刚开始接触Python多线程编程的时候我总觉得Condition这个东西有点鸡肋。明明有Lock、Event、Semaphore这些熟悉的工具为什么还要多一个Condition直到有一次我需要处理一个“生产者-消费者”的场景——一个线程往缓冲区里写数据另一个线程从里面读但读线程必须在有数据的时候才能读而且两个线程都不能同时操作缓冲区。用Lock可以解决互斥问题但怎么通知消费者“数据来了”呢这时候我才意识到Condition的真正价值。它到底是什么Condition可以理解为一个“带条件的锁”。在Python的threading模块里它本身不是一个完全独立的东西而是基于Lock或者RLock构建的。就像装修房子时Lock是一把普通的门锁Condition则是在门锁上加了个门铃——你不仅能锁门确保一次只有一个人进屋还能让人按门铃通知屋里的人“外面有人”。用更生活化的例子来说假设你有一个共享的厨房缓冲区厨房只能同时进去一个人互斥。生产者在厨房里做好菜写入数据消费者在厨房外等着吃读取数据。普通锁能做到的是生产者进去的时候把门锁上消费者进不去生产者出来开了锁消费者才能进去。但问题是消费者进去以后发现厨房是空的那就白跑一趟。Condition解决了这个问题消费者先上锁进厨房检查如果没菜就“等待”并自动释放锁直到生产者做好菜后“通知”他。代码层面上Condition内部维护一个锁和一个等待队列。当线程调用wait()时它会释放锁并进入等待状态直到被其他线程的notify()或notify_all()唤醒。唤醒后它会重新尝试获取锁然后继续执行。它能做什么Condition最典型的应用场景就是生产者-消费者模式但远不止于此。任何需要“等待某个条件成立”的多线程场景都可以用到它。比如网络爬虫里一个线程下载URL另一个线程解析HTML解析线程必须等到下载完成才能工作日志处理中写入线程不断往缓冲区写日志读取线程等到缓冲区满了再批量写入磁盘任务调度里工作线程等待任务到来主线程添加任务后通知它们它的核心价值在于避免了“忙等待”busy waiting。如果没有Condition你可能会用while True循环不断检查条件这样会占用大量CPU资源。有了Condition线程可以在等待时真正休眠直到被通知才醒来。实际怎么用直接看一个生产者和消费者的例子可能会更清楚。假设我们要写一个处理URL的脚本一个线程生成URL另一个线程下载页面importthreadingimporttimeimportrandomclassURLWorkPipeline:def__init__(self):self.url_queue[]self.conditionthreading.Condition()self.stop_signalFalsedefproducer(self,urls):forurlinurls:withself.condition:self.url_queue.append(url)print(f生产者添加了:{url})# 通知消费者有新的URL了self.condition.notify()time.sleep(random.uniform(1,3))# 全部完成后发送停止信号withself.condition:self.stop_signalTrueself.condition.notify_all()defconsumer(self):whilenotself.stop_signalorself.url_queue:withself.condition:# 当队列为空且没有停止信号时等待whilenotself.url_queueandnotself.stop_signal:self.condition.wait()ifself.url_queue:urlself.url_queue.pop(0)print(f消费者处理了:{url})# 如果队列空了并且有停止信号就退出ifnotself.url_queueandself.stop_signal:break# 使用示例pipelineURLWorkPipeline()producer_threadthreading.Thread(targetpipeline.producer,args[[url1,url2,url3]])consumer_threadthreading.Thread(targetpipeline.consumer)producer_thread.start()consumer_thread.start()这里有几个关键点要注意。wait()必须放在while循环里而不是if判断。这是因为线程被唤醒后条件可能已经不成立比如被其他线程抢走了资源。这叫“虚假唤醒”在Python的官方文档里也明确提到这一点。有些新手会犯这个错误导致程序偶尔出问题。还有就是notify()和notify_all()的选择。如果只有一个等待线程用notify()就够了如果有多个而且所有等待线程都可以处理新的条件可以用notify_all()。但要注意notify()不会释放锁它只是告诉等待的线程“可以试试重新获取锁了”。所以必须在释放锁的上下文里调用它也就是with块结束前。最佳实践用Condition的时候有几个不那么显而易见但很重要的原则。第一个尽量让wait()的条件检查变得简单。不要在while循环里做复杂的计算或I/O操作因为每次被唤醒都会重新执行一次。如果需要检查多个条件可以把它封装成一个函数但函数本身要保持轻量。第二个考虑使用超时机制。wait(timeoutseconds)可以避免线程无限期等待下去。这在生产环境里很重要因为万一通知丢失虽然理论上不会线程就会卡死。设置一个合理的超时时间然后在超时后重新检查条件或做其他处理。第三个注意锁的粒度。Condition的锁不仅保护条件本身还保护你修改共享数据的操作。所以用with self.condition包裹的区域应该尽量短只包含必要的操作。比如在生产者例子里添加URL和打印日志都放在里面但如果日志很重可以考虑只保护队列操作通知之后再做日志。第四个如果你的代码需要多次通知考虑用notify_all()而不是多次调用notify()。因为每次notify()只唤醒一个线程你可能需要多次调用才能唤醒所有需要的线程而且被唤醒的线程之间还要竞争锁效率反而不高。和其他同步工具的对比Condition和Event看起来有点像都能实现线程间的通知。但区别在于Event更像一个一次性开关——设定了就永久有效即使线程后来才去检查也能收到信号。Condition则不同通知只在当时有效如果线程没在等待通知就丢失了。所以Event适合“通知状态已经改变”的场景而Condition适合“通知现在有条件可以处理了”的场景。和Semaphore信号量相比Semaphore控制的是资源数量比如同时允许几个线程访问某个池子。Condition更灵活它可以基于任意条件来等待和通知。不过如果只是简单限制并发数Semaphore更轻量。还有一点很多人不知道Condition可以和RLock可重入锁配合使用。如果你有递归调用或者嵌套的锁需求可以用threading.Condition(threading.RLock())。比如一个递归函数里需要等待条件但已经持有了锁这时如果用普通Lock就会死锁。说到死锁Condition同样容易踩坑。最常见的错误就是忘记在wait()外面套while循环或者在持有锁的情况下调用了notify()后立即释放锁但被唤醒的线程需要获取锁才能继续。这些细节都需要在实际写代码时格外留意。总的来说Condition是一个比Lock稍微复杂一点但也比Event和Semaphore更灵活的同步工具。它适合那些需要“等待某个条件成立”的场景尤其是当这个条件涉及共享资源的复杂状态时。虽然Python的asyncio和queue模块提供了更高层次的封装但在基本的线程同步里理解Condition还是能写出更高效、更可控的并发代码。