python进阶–线程-通信
1. 变量竞争
1.1 资源竞争问题
当两个线程对公共资源进行修改时,往往会出错。
下面是经典的线程资源竞争问题:
from threading import Thread
def add():
global count
for i in range(1000000):
count +=1
def sub():
global count
for i in range(1000000):
count -= 1
def test():
th_add = Thread(target=add)
th_sub = Thread(target=sub)
th_add.start();th_sub.start()
th_add.join();th_sub.join()
print(count)
if __name__ == '__main__':
count = 0
test()
经测试输出的结果并非预料的0而是一个不确定的数。详情请搜索python线程资源竞争问题
1.2 解决方案
对于资源竞争问题,通常采用锁机制解决
from threading import Thread, Lock
def add():
global count
for i in range(1000000):
lock.acquire() #所有原子操作均在锁内进行
count +=1
lock.release()
def sub():
global count
for i in range(1000000):
lock.acquire()
count -= 1
lock.release()
def test():
th_add = Thread(target=add)
th_sub = Thread(target=sub)
th_add.start();th_sub.start()
th_add.join();th_sub.join()
print(count)
if __name__ == '__main__':
count = 0
lock = Lock() #获得锁
test()
锁机制的原理是:当lock.acquire
方法执行后,其他线程不再获得cpu争夺权,直到lock.release
方法执行后再次争夺cpu。即暂时停止多线程,等锁内操作执行完为止。
1.3 队列
对于线程的锁机制,Python的queue模块提供很好的底层帮助。我们先了解生产者-消费者模型。
把一个需要进程通信的问题分开考虑
① 生产者:只需要往队列里面放置产品(不需要关心如何消费,也不用关心是谁消费)
② 消费者:只需要从队列里面拿去产品(不需要关心如何生产,也不用关心是谁生产)
1.4 queue
queue
模块实现多生产者,多消费者队列。当信息必须安全的在多线程之间交换时,它在线程编程中是特别有用的。此模块中的 Queue
类实现了所有锁定需求的语义。
模块实现了三种类型的队列,它们的区别仅仅是条目取回的顺序。在 FIFO 队列(Queue)中,先添加的产品先取回。在 LIFO 队列(LifoQueue)中,最近被添加的条目先取回(操作类似一个堆栈)。优先级队列(PriorityQueue)中,条目将保持排序( 使用 heapq
模块 ) 并且最小值的条目第一个返回。
1.5 Queue
class Queue:
def __init__(self, maxsize=0):
pass
maxsize是一个整数,表示队列中允许存放的产平数目上限,如果产品数达到上限,那么停止产品压入队列。若maxsize<=0则队列内允许存放产品数目上线为无穷大。
队列对象 (Queue, LifoQueue, PriorityQueue)均提供如下方法:
1. qsize
返回队列大致长度。注意:返回值**>0**不保证后续的 get() 不被阻塞,返回值 <maxsize 不保证后续的 put() 不被阻塞。
2. empty
如果队列为空,返回 True
,否则返回 False
。如果 empty() 返回 True
,不保证后续调用的 put() 不被阻塞。类似的,如果 empty() 返回 False
,也不保证后续调用的 get() 不被阻塞。
3. full
如果队列是满的返回 True
,否则返回 False
。如果 full() 返回 True
不保证后续调用的 get() 不被阻塞。类似的,如果 full() 返回 False
也不保证后续调用的 put() 不被阻塞。
4 put
def put(item, block=True, timeout=None):
pass
item:产品
block:阻塞/非阻塞 标志,默认为true即阻塞
timeout:允许的延时时间
方法用于将产品item压入队列,对于队列已满情况:
若 block=True,timeout=None:阻塞式、不限时,所以等待队列有空位(消费者消费)就将产品压入队列。
若 block=True,timeout=x:阻塞式、限时x秒,若x秒内无空位,抛出异常Full
若 block=False:非阻塞式,限时已经无意义,只要队列满了,压入失败立刻抛出异常Full
5. put_nowait
相当于put( item, False)
6. task_done
队列如何得知所有的产品都被接收和处理完毕呢,简单说队列有一个计数器,记录正在处理的产品数目,生产者压入一个产品计数器加1,消费者处理一个产品,计数器减一。那么队列如何得知消费者处理产品了呢?
消费者处理一个产品后,调用task_down方法,向队列发送消息,通知队列产品已处理,计数器减一。
所以,需要确保队列为空(比如使用join方法)时,每当消费者处理一个产品后都要调用他。
7. join
阻塞至队列中所有的产品都被接收和处理完毕。
举个栗子:
使用task_down
from threading import Thread, current_thread
from queue import Queue
import time
import random
qsize = 3
q = Queue(qsize)
def customer():
while(True):
time.sleep(0.5) #延时一下,便于查看
print(current_thread().name, q.get());q.task_done()
def producer():
while(True):
c = random.choice('农夫山泉有点甜')
q.put(c)
print(current_thread().name, c) #每次向对列压入两个产品
c = random.choice('农夫山泉有点甜')
q.put(c)
print(current_thread().name, c)
q.join() #队列不空就会阻塞不在运行
thp = Thread(target=producer, name='pro') #生产者
thc = Thread(target=customer, name='cus') #消费者
thc.start()
thp.start()
pro 山
pro 泉
cus 山
cus 泉
pro 山
pro 泉
cus 山
cus 泉
……
q.join() 后生产者被阻塞,只有等到消费者将队列”清空“后才会继续,所以生产者执行一次–>生产者卡主–>消费者执行两次–>队列已空、生产者执行一次–>生产者卡主……
不使用task_down
from threading import Thread, current_thread
from queue import Queue
import time
import random
qsize = 3
q = Queue(qsize)
def customer():
while(True):
time.sleep(0.5) #延时一下,便于查看
print(current_thread().name, q.get())#;q.task_done() 不再使用
def producer():
while(True):
c = random.choice('农夫山泉有点甜')
q.put(c)
print(current_thread().name, c) #每次向对列压入两个产品
c = random.choice('农夫山泉有点甜')
q.put(c)
print(current_thread().name, c)
q.join() #队列不空就会阻塞不在运行
thp = Thread(target=producer, name='pro') #生产者
thc = Thread(target=customer, name='cus') #消费者
thc.start()
thp.start()
pro 夫
pro 泉
cus 夫
cus 泉
#此时卡住了
卡主的原因很简单,消费者虽然处理了产品,队列”清空“,但是未调用task_down,计数器未减一,导致队列认为自己没清空,所以生产者在q.join()这一步永久堵塞,而消费者取不到产品也永久堵塞。
总之,join()的时候一定要用task_down,当然可以看出task_down对消费者消费产品无直接影响(消费者依旧可以取)。
8. get
def get(block=True, timeout=None):
pass
从队列中取出一个产品
参数block和timeout的含义与put方法雷同,用于处理空队列状况。
9 get_nowait
相当于get(False)