生产者消费者模式、线程
一、生产者消费者模式
1、理论:
-生产者模式是通过一个容器来解决(类程序)生产者和消费者的强耦合问题。
-生产者和消费者之间并不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产数据后不用等待消费者处理,直接扔进阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里面取
-阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的工作能力,来提高程度的整体处理数据的速度。
2、实现方式:
生产者<——>队列<——>消费者
3、代码实现
# 初级模型:
from multiprocessing import Process,Queue
import time, random
def producer(name,food,q):
for i in range(10):
data = f"{name}生产了{food}"
time.sleep(random.randint(1,3))
print(data)
q.put(food)
def consumer(name,q):
while True:
food = q.get()
# 判断一下food是否是None,如果是就结束进程,避免一直等待
if food is None: break
time.sleep(random.randint(1,3))
print(f'{name}消费了{food}')
if __name__ == '__main__':
q = Queue()
p = Process(target=producer, args=('jason','饺子',q))
p.start()
c = Process(target=consumer, args=('jack',q))
c.start()
# 为避免消费者因为q里面没有数据而卡在q.get()上面,我们加一个q.put(None)
p.join()
q.put(None) # 发送结束信号
4、JoinableQueue([maxsize]):
就像是一个Queue对象,但是队列允许项目的使用者通知生产者项目已经被成功处理,通知进程是使用共享的信号和条件变量是实现的。
方法介绍:
JoinableQueue的实例q除了和Queue对象相同的方法之外还具有:
q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理,如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常,
q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。
阻塞将持续到队列中的每一个项目均调用q.task_done()方法为止
-代码实现:
from multiprocesssing import Process,JoinableQueue
import time,random
def producer(name,food,q):
for i in range(10):
time.sleep(random.randint(1,3)
res = f"{food}{i}"
print( f"{name}生产了{food}")
q.put(res)
q.join()
def consumer(name,q):
while True:
food = q.get()
time.sleep(random.randint(1,3))
print(f'{name}消费了{food}')
q.task_done()
if __name__ == '__main__':
q = JoinableQueue()
p1 = Process(target=producer, args=('jason','饺子',q))
p2 = Process(target=producer, args=('cc','蛋糕',q))
p3 = Process(target=producer, args=('小芳','面包',q))
c1 = Process(target=consumer, args=('jack',q))
c2 = Process(target=consumer, args=('xx',q))
c1.daemon = True
c2.daemon = True
pp = [p1,p2,p3,c1,c2]
for i in pp:
i.start()
# 为避免消费者因为q里面没有数据而卡在q.get()上面,我们加一个q.put(None)
p1.join()
p2.join()
p3.join()
print('主')
# 主进程等-->p1,p2,p3等-->c1,c2
# p1,p2,p3结束了,证明c1,c2肯定也全部收完了,也就没有继续运行的必要了,所以设置成守护进程
5、总结JoinableQueue()
每放一个值,数字加一
取值不会减一,q.task_done()
q.join() 一直阻塞,当q没有值了,才继续运行
二、线程理论
计算机相当于是医德大工厂,工厂里面有一个个的车间(进程),有很多人(线程)干不同的事情
真正干活的是线程--》线程是CPU调度的最小单位
每一个进程中至少要有一个线程--》进程是资源分配的最小单位
相比进程,线程开销更小,更轻量级
三、开启线程的两种方式
- 第一种
from threading import Thread
import time
def task():
print('开始')
time.sleep(1)
print('结束')
if __name__ == '__main__':
t = Thread(target=task, )
t.start()
print('主')
- 第二种
from threading import Tread
import time
class MyThread(Thread):
def run(self):
print('开始')
time.sleep(1)
print('结束')
if __name__ == '__main__':
t = MyThread()
t.start()
print('主')
四、线程对象的join方法
作用:等待子线程执行结束
from threading import Thread
import time
def task(n):
print('开始')
time.sleep(n)
print('结束')
if __name__ == '__main__':
t = Thread(target=task, args=(2,) )
t.start()
t.join() # 等子线程t执行完成,主线程才能继续
print('主')
五、同一线程下的多个线程数据共享
在线程内修改全局变量是可以在全局查找到的
from threading import Thread
import time
x = 10
def task(n):
global x
x = n
print('开始')
time.sleep(n)
print('结束')
if __name__ == '__main__':
t1 = Thread(target=task, args=(15,))
t1.start()
t2 = Thread(target=task, args=(20,))
t2.start()
t1.join()
t2.join()
print(x) # 20
print('主')
六、线程对象及其他方法
-Thread实例对象的方法
t.isAlive():返回线程是否还在运行(不推荐使用),推荐使用t.is_alive()
t.getName():返回线程名,同t.name
t.setName():设置线程名
t.ident :可以当作是线程的id号(线程是没有id号的,此方法主要用于区分不同线程)
-threading模块提供的方法
current_count():返回当前正在运行的线程数量
-代码演示:
from threading import Thread, current_thread,active_count
import time
import os
def task(n):
print('开始')
print(current_thread().name) # 打印线程名字
print(os.getpid()) # 打印进程id
time.sleep(n)
print('结束')
if __name__ == '__main__':
t1 = Thread(target=task,name='egon',args=(2,)) # 可以给线程命名
t2 = Thread(target=task,args=(8,)) # 不命名就默认命名为thread—1、thread-2...
t1.start()
t2.start()
t1.join()
print('---------',t1.is_alive()) # False
print('---------',t2.is_alive()) # True
# 当作线程id号
print('*********',t1.ident)
print('*********',t2.ident)
print(os.getpid())
print(active_count()) # 打印出2 ,开了两个线程,其中一个运行完毕,还有一个主线程
七、守护线程
无论是进程还是线程,都遵循守护进程/线程 会等待主进程/线程运行完毕后被销毁
**强调:**运行完毕不代表终止运行
# 对于主进程来说,运行完毕指的是主进程代码运行完毕
# 对于主线程来说,运行完毕指的是主线程所在的进程内,所有的非守护线程统统运行完毕,主线程才算运行完毕
代码演示:
from threading import Thread, current_thread, active_count
import time
def task(n):
print(f'{current_thread().name}开始')
time.sleep(n)
print(active_count())
print(f'{current_thread().name}结束')
if __name__ == '__main__':
t1 = Thread(target=task, name='fine', args=(10,))
t1.daemon = True
t1.start()
t2 = Thread(target=task, args=(2,))
t2.start()
print('主')
'''
# 运行结果:
fine开始
Thread-1开始主
3
Thread-1结束
'''
八、线程互斥锁
注意点:
1、线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,
但是如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立即交出来。
2、join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,
要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高
代码演示:
from threading import Thread, Lock
import time,random
x = 100
def task(mutex):
global x
# 在修改数据的时候加锁
mutex.acquire()
temp = x
time.sleep(0.1)
x = temp - 1
# 修改完以后释放锁,以便其他线程能再次抢到锁
mutex.release()
if __name__ == '__main__':
ll = []
mutex = Lock()
for i in range(10):
t = Thread(target=task,args=(mutex,)
t.start()
ll.append(t)
for i in ll:
i.join()
print(x) # 89
九、GIL全局解释器锁理论
#1 python的解释器有很多,cpython,jpython,pypy(python写的解释器)
#2 python的库多,库都是基于cpython写起来的,其他解释器没有那么多的库
#3 cpython中有一个全局大锁,每条线程要执行,必须获取到这个锁
#4 为什么会有这个锁呢?python的垃圾回收机制可能会将刚定义好还没有赋值的变量名直接回收掉
#5 python的多线程其实就是单线程
#6 某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行
# 7 总结:cpython解释器中有一个全局锁(GIL),线程必须获取到GIL才能执行,我们开的多线程,不管有几个cpu,同一时刻,只有一个线程在执行(python的多线程,不能利用多核优势)
# 8 如果是io密集型操作:开多线程
# 9如果是计算密集型:开多进程
以上两句话,只针对与cpython解释器