并发编程二
并发编程
生产者消费者模型
-
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
-
一类负责生产数据(生产者) 一类负责处理数据(消费者) 生产者<-->队列<——>消费者 -Queue,redis,rabbitmq,kafka -解耦合,队列是微服务的基础
-
import time import random from multiprocessing import Process, Queue,JoinableQueue def producer(name, food, q): for i in range(10): data = '%s 制造了%s' % (name, food) # 模拟制造食物延迟 time.sleep(random.randint(1, 3)) print(data) q.put(food) def consumer(name, q): while True: food = q.get() # 模拟吃食物延迟 time.sleep(random.randint(1, 3)) print('%s消费了%s' % (name, food)) q.task_done() # 把队列中维护的数字减一 if __name__ == '__main__': # q = Queue() # 内部为何了一个数字,放一个数字会加一 # 消费一个数字减一 q = JoinableQueue() # 创造一个生产者 p = Process(target=producer, args=('egon', '包子', q)) p.start() p1 = Process(target=producer, args=('alex', '泔水', q)) p1.start() # 创造一个消费者 c = Process(target=consumer, args=('鸡哥', q)) # c.daemon = True c.start() c1 = Process(target=consumer, args=('王铁蛋', q)) # c1.daemon = True c1.start() c2 = Process(target=consumer, args=('李铁柱', q)) # c2.daemon = True c2.start() # 主结束,消费进程也结束,把每个消费进程都设置成守护进程 # 等待所有生产者生产结束,主进程再结束 p.join() p1.join() q.join() # 会一直等待q队列中数据没有了,才继续往下走 print('生产者结束了,主进程结束') # 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() # 对象.start()启动线程 # print('主') # 第二种,通过类继承的方式 # from threading import Thread # import time # # class MyThread(Thread): # def run(self): # print('开始') # time.sleep(1) # print('结束') # if __name__ == '__main__': # t=MyThread() # t.start() # print('主')
-
线程对象join方法和进程方法一样,需要等待子线程执行结束,同一个进程下的线程数据之间是共享的
-
线程t.name t.getName()线程中没有pid号,只有名字,如果未指定名字,则会默认 当前进程下有几个线程存活active_count t1.is_alive() 当前线程是否存活 t1.ident 当作是线程id号
-
进程互斥锁
from threading import Thread,Lock import time import random money = 99 def task(n,mutex): global money # 在修改数据的时候,枷锁 mutex.acquire() temp = money time.sleep(0.1) money = temp - 1 # 修改完以后,释放锁,其它线程就能再次抢到锁 mutex.release() if __name__ == '__main__': ll=[] mutex=Lock() for i in range(10): t = Thread(target=task, args=(i,mutex)) t.start() ll.append(t) for i in ll: i.join() print(money) # 不同线程要修改同一个数据,要加锁,让并行变成串行,牺牲了效率,保证了数据安全,悲观锁,乐观锁,分布式锁 # 乐观锁:总是认为不会产生并发问题,因此不会上锁,在修改数据时不会认为其他线程也正在修改,一旦碰到其他线程正在修改,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。 # 悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁证,同一时刻只有一个线程能操作数据,其他线程则会被block。 # 分布式锁:redis锁:通过查询获取key的方式实现加锁,可以理解为是悲观锁的一种,每次对数据进行操作,先查询key,如果查询到key,即当前资源被占用,不能进行操作;如果要加锁的那个锁key不存在的话,你就可以进行加锁;执行lock.unlock(),就可以释放分布式锁;
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密集型操作:开多线程,如果是计算密集型:开多进程,以上两句话,只针对与cpython解释器
-
验证GIL锁的存在
from threading import Thread from multiprocessing import Process def task(): while True: pass if __name__ == '__main__': for i in range(6): # 根据自己的电脑核数来定 #t=Thread(target=task) # 因为有GIL锁,同一时刻,只有一条线程执行,所以cpu不会满 t=Process(target=task) # 由于是多进程,进程中的线程会被cpu调度执行,6个cpu全在工作,就会跑满 t.start()
-
GIL锁和普通锁的区别
# GIL锁是不能保证数据的安全,普通互斥锁来保证数据安全 from threading import Thread, Lock import time mutex = Lock() money = 100 def task(): global money mutex.acquire() #未加锁时,在每个进程进来的时候temp都是100,所以最后打印出来的是99 temp = money time.sleep(1) money = temp - 1 mutex.release() if __name__ == '__main__': ll=[] for i in range(10): t = Thread(target=task) t.start() # t.join() # 会怎么样?变成了串行,不能这么做 ll.append(t) for t in ll: t.join() print(money)
i/o密集型和计算密集型
计算密集型 # 指cpu每次在调度时每次程序都处于就绪态,可以直接工作,任务分解多个进程,最终结果通过回调函数返回,最终通过一系列复杂的计算,得出最终结果。
# def task():
# count = 0
# for i in range(100000000):
# count += i
# if __name__ == '__main__':
# ctime = time.time()
# ll = []
# for i in range(10):
# t = Thread(target=task) # 开线程:42.68658709526062
# # t = Process(target=task) # 开进程:9.04949426651001
# t.start()
# ll.append(t)
#
# for t in ll:
# t.join()
# print(time.time()-ctime)
## io密集型
def task():
time.sleep(2)
if __name__ == '__main__':
ctime = time.time()
ll = []
for i in range(400):
t = Thread(target=task) # 开线程:2.0559656620025635
# t = Process(target=task) # 开进程:9.506720781326294
t.start()
ll.append(t)
for t in ll:
t.join()
print(time.time()-ctime)
# i/o密集型开多线程,计算密集型开多进程,多核多线程比单核多线程更差
死锁现象(哲学家就餐问题)
-
是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程或者线程称为死锁进程或线程,如下就是死锁 张三拿到了A锁,等B锁,李四拿到了B锁,等A锁;或者张三拿了两次A锁
from threading import Thread, Lock import time mutexA = Lock() mutexB = Lock() def eat_apple(name): mutexA.acquire() print('%s 获取到了a锁' % name) mutexB.acquire() print('%s 获取到了b锁' % name) print('开始吃苹果,并且吃完了') mutexB.release() print('%s 释放了b锁' % name) mutexA.release() print('%s 释放了a锁' % name) def eat_egg(name): mutexB.acquire() print('%s 获取到了b锁' % name) time.sleep(2) mutexA.acquire() print('%s 获取到了a锁' % name) print('开始吃鸡蛋,并且吃完了') mutexA.release() print('%s 释放了a锁' % name) mutexB.release() print('%s 释放了b锁' % name) if __name__ == '__main__': ll = ['egon', 'alex', '铁蛋'] for name in ll: t1 = Thread(target=eat_apple, args=(name,)) t2 = Thread(target=eat_egg, args=(name,)) t1.start() t2.start() # 产生了死锁现象
递归锁(可重入),同一个人可以多次acquire,每acquire一次,内部计数器加1,每relaese一次,内部计数器减一 # 只有计数器不为0,其他人都不获得这把锁 from threading import Thread, Lock,RLock import time mutexA = mutexB =RLock() def eat_apple(name): mutexA.acquire() print('%s 获取到了a锁' % name) mutexB.acquire() print('%s 获取到了b锁' % name) print('开始吃苹果,并且吃完了') mutexB.release() print('%s 释放了b锁' % name) mutexA.release() print('%s 释放了a锁' % name) def eat_egg(name): mutexB.acquire() print('%s 获取到了b锁' % name) time.sleep(2) mutexA.acquire() print('%s 获取到了a锁' % name) print('开始吃鸡蛋,并且吃完了') mutexA.release() print('%s 释放了a锁' % name) mutexB.release() print('%s 释放了b锁' % name) if __name__ == '__main__': ll = ['egon', 'alex', '铁蛋'] for name in ll: t1 = Thread(target=eat_apple, args=(name,)) t2 = Thread(target=eat_egg, args=(name,)) t1.start() t2.start()
Semaphore信号量
-
Semaphore:信号量可以理解为多把锁,同时允许多个线程来更改数据
from threading import Thread,Semaphore import time import random sm=Semaphore(3) # 数字表示可以同时有多少个线程操作 def task(name): sm.acquire() print('%s 正在蹲坑'%name) time.sleep(random.randint(1,5)) sm.release()
Event事件
-
一些线程需要等到其他线程执行完成之后才能执行,类似于发射信号,比如一个线程等待另一个线程执行结束再继续执行
from threading import Thread, Event import time event = Event() def girl(name): print('%s 现在不单身,正在谈恋爱'%name) time.sleep(10) print('%s 分手了,给屌丝男发了信号'%name) event.set() def boy(name): print('%s 在等着女孩分手'%name) event.wait() # 只要没来信号,就卡在者 print('女孩分手了,机会来了,冲啊') if __name__ == '__main__': lyf = Thread(target=girl, args=('刘亦菲',)) lyf.start() for i in range(10): b = Thread(target=boy, args=('屌丝男%s号' % i,)) b.start()
from threading import Thread, Event import time import os event = Event() # 获取文件总大小 size = os.path.getsize('a.txt') def read_first(): with open('a.txt', 'r', encoding='utf-8') as f: n = size // 2 # 取文件一半,整除 data = f.read(n) print(data) print('我一半读完了,发了个信号') event.set() def read_last(): event.wait() # 等着发信号 with open('a.txt', 'r', encoding='utf-8') as f: n = size // 2 # 取文件一半,整除 # 光标从文件开头开始,移动了n个字节,移动到文件一半 f.seek(n, 0) data = f.read() print(data) if __name__ == '__main__': t1=Thread(target=read_first) t1.start() t2=Thread(target=read_last) t2.start()
线程Queue
进程queue和线程不是一个 from multiprocessing import Queue
线程queue from queue import Queue,LifoQueue,PriorityQueue
# 线程间通信,因为共享变量会出现数据不安全问题,用线程queue通信,不需要加锁,内部自带 queue是线程安全的
三种线程Queue
-Queue:队列,先进先出
-PriorityQueue:优先级队列,谁小谁先出
-LifoQueue:栈,后进先出
PriorityQueue:数字越小,级别越高
# q=PriorityQueue(3)
# q.put((-10,'金蛋'))
# q.put((100,'银蛋'))
# q.put((101,'铁蛋'))
# # q.put((1010,'铁dd蛋')) # 不能再放了
# print(q.get()) # 先取出来的是级别小的
# print(q.get())
# print(q.get())
线程池 进程池
-
不管是开进程还是开线程,不能无限制开,通过池,假设池子里就有10个,不管再怎么开,永远是这10个,进程池类似,除了导入的类不一样
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor from threading import Thread import time import random pool = ThreadPoolExecutor(5) # 数字是池的大小 # pool = ProcessPoolExecutor(5) # 数字是池的大小 def task(name): print('%s任务开始' % name) time.sleep(random.randint(1, 4)) print('任务结束') return '%s 返回了'%name def call_back(f): print(f.result()) if __name__ == '__main__': for i in range(10): # 起了10个线程 # 向线程池中提交一个任务,等任务执行完成,自动回到到call_back函数执行 pool.submit(task,'屌丝男%s号' % i).add_done_callback(call_back)