python基础之进程、线程、协程
引子
进程
线程(优先阅读)
协程
进程
概念 :就是一个程序在一个数据集上的一次动态执行过程(本质上来讲,就是运行中的程序(代指运行过程),程序不运行就不是进程) 抽象概念
组成 :
1、程序:我们编写的程序用来描述进程要完成哪些功能以及如何完成
2、数据集:数据集则是程序在执行过程中所需要使用的资源
3、进程控制块:进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
阐释 :进程与进程之间都占用的是独立的内存块,它们彼此之间的数据也是独立的
优点 :同时利用多个CPU,能够同时进行多个操作
缺点 :耗费资源(需要重新开辟内存空间)
构造方法:
Process([group [, target [, name [, args [, kwargs]]]]])
group: 线程组,目前还没有实现,库引用中提示必须是None;
target: 要执行的方法;
name: 进程名;
args/kwargs: 要传入方法的参数。
实例方法:
is_alive():返回进程是否在运行。
join([timeout]):阻塞当前上下文环境的进程程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。
start():进程准备就绪,等待CPU调度
run():strat()调用run方法,如果实例进程时未制定传入target,这star执行t默认run()方法。
terminate():不管任务是否完成,立即停止工作进程
属性:
daemon:和线程的setDeamon功能一样
name:进程名字。
pid:进程号。
创建进程的方式有俩种
一,通过调用模块的方式来创建线程
# 进程模块
import multiprocessing
import time
def f1():
start = time.time()
sum = 0
for n in range(100000000):
sum += n
print(sum)
print("data:{}".format(time.time() - start))
if __name__ == '__main__': # windows在调用进程的时候,必须加这句话,否则会报错
li = []
p1 = multiprocessing.Process(target=f1)
li.append(p1)
p2 = multiprocessing.Process(target=f1)
li.append(p2)
for p in li:
p.start()
for i in li:
i.join()
print("ending...")
二,通过继承类的方式(推荐)
import multiprocessing
class Process(multiprocessing.Process):
def run(self):
sum = 0
for n in range(100000000):
sum += n
print(sum)
li = []
for i in range(2):
p = Process()
li.append(p)
if __name__ == '__main__':
for p in li:
p.start()
for i in li:
i.join()
print("ending")
进程之间的通信
创建进程模块的下队列(Queue)
# 进程之间的通信 Queue
from multiprocessing import Queue, Process, Pipe
import os,time,random
def write(q):
print("process to write{}".format(os.getpid()))
for value in ["A","B","C"]:
print("Put {} to queue...".format(value))
q.put(value)
time.sleep(random.random())
def read(q):
print("process to read{}".format(os.getpid()))
while True:
value = q.get(True)
print("Get {} from queue".format(value))
if __name__ == '__main__':
q = Queue()
pw = Process(target=write,args=(q,)) # 这里传输的q是copy的
pr = Process(target=read,args=(q,))
pw.start()
pr.start()
pw.join()
pr.terminate() # 强行终止进程(因为这个子进程定义了一个死循环)
管道(Pipe)
# 进程之间的通信 Pipe(类似于socket)
from multiprocessing import Queue, Process, Pipe
import os,time,random
# 说明Pipe的send是没有返回值的
pipe = Pipe()
# print(pipe)
def worker(pipe):
time.sleep(random.random())
for i in range(10):
print("worker send {}".format(pipe.send(i)))
def Boss(pipe):
while True:
print("Boss recv {}".format(pipe.recv()))
p1 = Process(target=worker,args=(pipe[0],))
p2 = Process(target=Boss,args=(pipe[1],))
if __name__ == '__main__':
p1.start()
p2.start()
上述实现了进程间的数据通信,那么进程可以达到数据共享么?Sure。
前一节中, Pipe、Queue 都有一定数据共享的功能,但是他们会堵塞进程, 这里介绍的两种数据共享方式都不会堵塞进程, 而且都是多进程安全的。
A manager object returned by Manager()
controls a server process which holds Python objects and allows other processes to manipulate them using proxies.
A manager returned by Manager()
will support types list
, dict
, Namespace
, Lock
, RLock
, Semaphore
, BoundedSemaphore
, Condition
, Event
, Barrier
, Queue
, Value
and Array
.
由上述英文我们了解到,通过Manager()可以实现进程上的数据共享,并且支持的类型也由很多,接下来看代码
from multiprocessing import Process, Manager
def f(d,l,n):
d["name"] = "alex"
d[n] = "1"
l.append(n)
if __name__ == '__main__':
with Manager() as manager: # 类似于文件操作的with open(...)
d = manager.dict()
l = manager.list(range(5))
print(d,l)
p_list = []
for n in range(10):
p = Process(target=f,args=(d, l, n))
p.start()
p_list.append(p)
for p in p_list:
p.join() # 这儿的join必须加
print(d)
print(l)
# 关于数据共享的进程等待的问题,鄙人作出一些自己的理解
# 多核CPU的情况下,进程间是可以实现并行的,当然每个核处理的速度又有极其细微的差异性,速度处理稍慢些的进程在还在对数据进行处理的候,同时又想要得到数据了,自然会出现错误,所以要等待进程处理完这份数据的时候再进行操作
from multiprocessing import Process, Manager
def func(n,a):
n.value = 50
for i in range(len(a)):
a[i] += 10
if __name__ == '__main__':
with Manager() as manager:
num = manager.Value("d", 0.0)
ints = manager.Array("i", range(10))
p = Process(target=func,args=(num,ints))
p.start()
p.join()
print(num)
print(ints)
输出
Value('d', 50)
array('i', [10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
# 共享内存有两个结构,一个是 Value, 一个是 Array,这两个结构内部都实现了锁机制,因此是多进程安全的。
# Value 和 Array 都需要设置其中存放值的类型,d 是 double 类型,i 是 int 类型,具体的对应关系在Python 标准库的 sharedctypes 模块中查看。
# 上面的共享内存支持两种结构 Value 和 Array, 这些值在主进程中管理,很分散。 Python 中还有一统天下,无所不能的Manager,专门用来做数据共享。 其支持的类型非常多。
进程同步
Lock
锁是为了确保数据一致性,比如读写锁,每个进程给一个变量增加 1 ,但是如果在一个进程读取但还没有写入的时候,另外的进程也同时读取了,并写入该值,则最后写入的值是错误的,这时候就需要锁。
# 为什么引申进程同步
# 数据的一致性
import time
from multiprocessing import Lock, Process
def run(i, lock):
with lock: # 自动获得锁和释放锁
time.sleep(1)
print(i)
if __name__ == '__main__':
lock = Lock()
for i in range(10):
p = Process(target=run,args=(i,lock,))
p.start()
Lock 同时也实现了 ContextManager API, 可以结合 with 语句使用, 关于 ContextManager, 请移步 Python 学习实践笔记 装饰器 与 context 查看。
Semaphore
Semaphore 和 Lock 稍有不同,Semaphore 相当于 N 把锁,获取其中一把就可以执行了。 信号量的总数 N 在构造时传入,s = Semaphore(N)
。 和 Lock 一样,如果信号量为0,则进程堵塞,直到信号大于0。
进程池
如果有50个任务要去执行,CPU只有4核,那创建50个进程完成,其实大可不必,徒增管理开销。如果只想创建4个进程,让它们轮流替完成任务,不用自己去管理具体的进程的创建销毁,那 Pool 是非常有用的。
Pool 是进程池,进程池能够管理一定的进程,当有空闲进程时,则利用空闲进程完成任务,直到所有任务完成为止
1
2
3
4
5
6
7
8
|
def
func(x):
``return
x``*``x
if
__name__ ``=``=
'__main__'``:
``p_pool ``=
pool.Pool(``4``)
``result ``=
p_pool.``map``(func,``range``(``8``))
``print``(result)
# Pool 进程池创建4个进程,不管有没有任务,都一直在进程池中等候,等到有数据的时候就开始执行。
—|—
从上面的例子来看貌似也看不出什么效果,那么接下来自定义一个进程池
关于进程池的API用法(并不是只有俩个哦)
apply (每个任务是排队进行,类似于串行失去意义)
apply_async (任务都是并发进行,并且可以设置回调函数) 进程的并发其实可以称之为并行了,可以利用到多核CPU
import os,time
from multiprocessing import pool,Process
def run(n):
# print(os.getpid())
time.sleep(1)
print(n)
return n # 该函数的返回值,是回调函数的所要传入的值
def bar(args):
pass
# print("bar {}".format(args))
# print(os.getpid())
if __name__ == '__main__':
p_pool = pool.Pool(5) # 设置进程池中的最大放置
for n in range(100):
# 回调函数,就是某个函数执行成功或结束执行的函数
p_pool.apply_async(func=run,args=(n,),callback=bar)
p_pool.close() # 进程的关闭和等待是有顺序的
p_pool.join()
print("ending")
# 看看 Pool 的执行流程,有三个阶段。第一、一个进程池接收很多任务,然后分开执行任务;第二、不再接收任务了;第三、等所有任务完成了,回家,不干了。
# 这就是上面的方法,close 停止接收新的任务,如果还有任务来,就会抛出异常。 join 是等待所有任务完成。 join 必须要在 close 之后调用,否则会抛出异常。terminate 非正常终止,内存不够用时,垃圾回收器调用的就是这个方法。
线程
概念 :线程是应用程序中工作的最小单元,或者又称之为微进程。
组成 :它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
阐释 :线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。线程可以共享(调用)进程的数据资源
优点 :共享内存,IO操作时候,创造并发操作
缺点 :“…”(中国文化的博大精深的带引号)
关于多线程
多线程类似于同时执行多个不同程序,多线程运行有如下优点:
- 使用线程可以把占据长时间的程序中的任务放到后台去处理。
- 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
- 程序的运行速度可能加快
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
- 线程可以被抢占(中断)。
- 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) – 这就是线程的退让。
线程可以分为:
- 内核线程: 由操作系统内核创建和撤销。
- 用户线程: 不需要内核支持而在用户程序中实现的线程。
Python3 线程中常用的两个模块为:
- _thread
- threading(推荐使用)
thread 模块已被废弃。用户可以使用 threading 模块代替。所以,在 Python3 中不能再使用"thread" 模块。为了兼容性,Python3 将 thread 重命名为 “_thread”。
Python中使用线程有两种方式:函数或者用类来包装线程对象。
Python3 通过两个标准库 _thread 和 threading 提供对线程的支持。
_thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。
threading 模块除了包含 _thread 模块中的所有方法外,还提供的其他方法:
- threading.currentThread() : 返回当前的线程变量。
- threading.enumerate() : 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount() : 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
- run(): 用以表示线程活动的方法。
- start(): 启动线程活动。
- join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
- setDaemon(True): 守护主线程,跟随主线程退(必须要放在start()上方)
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
看了那么多废话,那么创建线程的方式有俩种,接下来看代码
一,通过调用模块的方式来创建线程(推荐使用)
import threading # 线程模块
import time
# 创建线程
def onepiece1(n):
print("路飞正在使用橡胶火箭炮%s,攻击力%s" %(time.ctime(),n))
time.sleep(3)
print("路飞结束该技能%s" %time.ctime())
def onepiece2(n):
print("艾尼路正在出雷神万击%s你,攻击力%s" %(time.ctime(),n))
time.sleep(5)
print("艾尼路结束该技能%s" %time.ctime())
if __name__ == '__main__':
thread_1 = threading.Thread(target=onepiece1,args=(10,)) # 创建子线程
thread_2 = threading.Thread(target=onepiece2,args=(9,))
thread_1.start()
# pyhton1.join()
thread_2.start()
thread_2.join() # 等待线程终止
print("ending Fighting")
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qUASgc2j-1692657261657)(https://common.cnblogs.com/images/copycode.gif)]
二,创建类通过继承的方式来创建线程
使用Threading模块创建线程,直接从threading.Thread继承,然后重写__init__方法和run方法:
import threading
import time
class MyThread(threading.Thread):
def __init__(self,num):
threading.Thread.__init__(self)
self.num = num
def run(self): # 定义每个线程要运行的函数
print("running on number:%s" %self.num)
time.sleep(3)
print("ending......")
if __name__ == '__main__':
t1 = MyThread(1) # 继承这个类,把1这个参数,传给num ,t1就是个线程对象
t2 = MyThread(2)
t1.start()
t2.start()
GIL
在知道线程的创建方式以及一些方法的使用后,引申一个cpython解释器的一个历史遗留问题,全局GIL锁
因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
当然了,也有通过别的途径提高执行效率,技术的道路上终无止境。
同步锁
多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。
使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步。
这两个对象都有 acquire 方法和 release 方法。
对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间。
def sub():
global num
thread_lock_A.acquire() # 获得锁,用于线程同步
tmep = num
time.sleep(0.001)
num = tmep - 1
thread_lock_A.release() # 释放锁,开启下一个线程
# 问题,加锁之后100个线程就变为了串行执行,锁内的代码
li = []
for i in range(100):
t = threading.Thread(target=sub)
t.start()
li.append(t)
for t in li:
t.join()
print("ending")
print(num)
__
线程的死锁和递归锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都
正在使用,所有这两个线程在无外力作用下将一直等待下去。
解决死锁就可以用递归锁
import threading,time
# lock_A = threading.Lock()
# lock_B = threading.Lock()
r_lock = threading.RLock()
class Mythread(threading.Thread):
def actionA(self):
r_lock.acquire()
print(self.name,time.ctime())
time.sleep(2)
r_lock.acquire()
print(self.name,time.ctime())
time.sleep(1)
r_lock.release()
r_lock.release()
def actionB(self):
r_lock.acquire()
print(self.name,time.ctime())
time.sleep(2)
r_lock.acquire()
print(self.name,time.ctime())
time.sleep(1)
r_lock.release()
r_lock.release()
def run(self):
self.actionA()
self.actionB()
li = []
for i in range(5):
t = Mythread()
t.start()
li.append(t)
for t in li:
t.join()
print("ending")
为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
**信号量(Semaphore):从意义上来讲,也可以称之为一种锁
**
信号量:指同时开几个线程并发
信号量用来控制线程并发数的,BoundedSemaphore或Semaphore管理一个内置的计数 器,每当调用acquire()时-1,调用release()时+1。
计数器不能小于0,当计数器为 0时,acquire()将阻塞线程至同步锁定状态,直到其他线程调用release()。(类似于停车位的概念)
BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数 器的值是否超过了计数器的初始值,如果超过了将抛出一个异常。
import threading,time
class myThread(threading.Thread):
def run(self): #启动后,执行run方法
if semaphore.acquire(): #加把锁,可以放进去多个(相当于5把锁,5个钥匙,同时有5个线程)
print(self.name)
time.sleep(5)
semaphore.release()
if __name__=="__main__":
semaphore=threading.Semaphore(5) #同时能有几个线程进去(设置为5就是一次5个线程进去),类似于停车厂一次能停几辆车
thrs=[] #空列表
for i in range(100): #100个线程
thrs.append(myThread()) #加线程对象
for t in thrs:
t.start() #分别启动
同步条件(Event)
简单了解
Event对象实现了简单的线程通信机制,它提供了设置信号,清楚信号,等待等用于实现线程间的通信。
1 设置信号
使用Event的set()方法可以设置Event对象内部的信号标志为真。Event对象提供了isSet()方法来判断其内部信号标志的状态。当使用event对象的set()方法后,isSet()方法返回真
2 清除信号
使用Event对象的clear()方法可以清除Event对象内部的信号标志,即将其设为假,当使用Event的clear方法后,isSet()方法返回假
3 等待
Event对象wait的方法只有在内部信号为真的时候才会很快的执行并完成返回。当Event对象的内部信号标志位假时,则wait方法一直等待到其为真时才返回。
import threading, time
class Boss(threading.Thread):
def run(self):
print("BOSS:今晚大家都要加班到22:00。")
print(event.isSet())
event.set()
time.sleep(5)
print("BOSS:<22:00>可以下班了。")
print(event.isSet())
event.set()
class Worker(threading.Thread):
def run(self):
event.wait()
print("Worker:哎……命苦啊!")
time.sleep(1)
event.clear()
event.wait()
print("Worker:OhYeah!")
if __name__ == "__main__":
event = threading.Event()
threads = []
for i in range(5):
threads.append(Worker())
threads.append(Boss())
for t in threads:
t.start()
for t in threads:
t.join()
Event内部包含了一个标志位,初始的时候为false。
可以使用使用set()来将其设置为true;
或者使用clear()将其从新设置为false;
可以使用is_set()来检查标志位的状态;
另一个最重要的函数就是wait(timeout=None),用来阻塞当前线程,直到event的内部标志位被设置为true或者timeout超时。如果内部标志位为true则wait()函数理解返回。
多线程利器——队列(queue)
因为列表是不安全的数据结构,所以引申了新的模块——队列
# 列表是不安全的数据结构 举个简单的例子
li = [1, 2, 3, 4, 5]
def remove():
while True:
xx = li[-1]
print(xx)
time.sleep(1)
li.remove(xx)
A = threading.Thread(target=remove)
B = threading.Thread(target=remove)
A.start()
B.start()
Python 的 queue 模块中提供了同步的、线程安全的队列类,包括 FIFO(先入先出)队列Queue , LIFO(后入先出)队列LifoQueue ,和 优先级队列 PriorityQueue 。
这些队列都实现了锁原语,能够在多线程中直接使用,可以使用队列来实现线程间的同步。
queue 模块中的常用方法:
- queue.qsize() 返回队列的大小
- queue.empty() 如果队列为空,返回True,反之False
- queue.full() 如果队列满了,返回True,反之False
- queue.full 与 maxsize 大小对应
- queue.get([block[, timeout]])获取队列,timeout等待时间
- queue.get_nowait() 相当queue.get(False)
- queue.put(item) 写入队列,timeout等待时间
- queue.put_nowait(item) 相当Queue.put(item, False)
- queue.task_done() 在完成一项工作之后,queue.task_done()函数向任务已经完成的队列发送一个信号
- queue.join() 实际上意味着等到队列为空,再执行别的操作
import queue
# 队列有三种模式
# 先进先出
qu = queue.Queue()
qu.put("alex")
qu.put(123)
qu.put({"age":18})
while True:
print(qu.get())
print("————————")
# 先进后出
qu = queue.LifoQueue()
qu.put("alex")
qu.put(123)
qu.put({"age":18})
while True:
print(qu.get())
print("————————")
# 优先级
q = queue.PriorityQueue(3) # 设定大小
q.put([1, "alex"])
q.put([3, 123])
q.put([2, {"age":18}])
# q.put([4,456]) # 如果装的大于设定大小,也会阻塞(等待)
# while True:
# print(q.get()[1]) # get当取不到值之后会等待
# print("————————")
print(q.qsize()) # 查看当前队列有多少个
print(q.empty()) # 判断是否为空
print(q.full()) # 判断是否为满
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
# 实例
import
queue
import
threading
import
time
go ``=
False
# 设定标识位
class
MyThread(threading.Thread):
``def
__init__(``self``, threadID, name, q):
``threading.Thread.__init__(``self``)
``self``.threadID ``=
threadID
``self``.name ``=
name
``self``.q ``=
q
``def
run(``self``):
``print``(``"开启线程:{}"``.``format``(``self``.name))
``process_data(``self``.name,``self``.q)
``print``(``"退出线程:{}"``.``format``(``self``.name))
def
process_data(thread_name,q):
``while
not
go:
``queue_lock.acquire() ``# 获得锁
``if
not
work_queue.empty(): ``# 如果队列为空返回True,反之False
``data ``=
q.get() ``# 向队列取值,先进先出
``queue_lock.release() ``# 释放锁
``print``(``"{} processing {}"``.``format``(thread_name,data))
``else``:
``queue_lock.release()
``time.sleep(``1``)
thread_list ``=
[``"Thread-1"``, ``"Thread-2"``, ``"Thread-3"``]
name_list ``=
[``"one"``, ``"two"``, ``"three"``, ``"four"``, ``"five"``]
queue_lock ``=
threading.Lock() ``# 同步锁
work_queue ``=
queue.Queue(``10``)
threads ``=
[]
threads_ID ``=
1
# 创建新线程
for
t ``in
thread_list:
``thread ``=
MyThread(threads_ID,t,work_queue) ``# 创建线程
``thread.start() ``# 启动线程
``threads.append(thread) ``# 追加线程对象到列表
``threads_ID ``+``=
1
# ID自加1
# 填充队列
queue_lock.acquire()
for
name ``in
name_list:
``work_queue.put(name) ``# 向队列填充
queue_lock.release()
# 等待队列清空. 清空返回True,则此循环会跳过
while
not
work_queue.empty():
``pass
# 改变状态,通知线程退出
go ``=
True
# 等待所有线程完成
for
t ``in
threads:
``t.join()
print``(``"退出主线程。"``)
—|—
生产者与消费者模型
在这个现实社会中,生活中处处充满了生产和消费.
什么是生产者消费者模型
在 工作中,可能会碰到这样一种情况:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。在生产者与消费者之间在加个缓冲区,形象的称之为仓库,生产者负责往仓库了进商 品,而消费者负责从仓库里拿商品,这就构成了生产者消费者模型。结构图如下
生产者消费者模型的 优点
1、解耦
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化, 可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
举个例子,我们去邮局投递信件,如果不使用邮筒(也就是缓冲区),你必须得把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须 得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了)。这就产生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。而邮筒相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。
2、支持并发
由于生产者与消费者是两个独立的并发体,他们之间是用缓冲区作为桥梁连接,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只需要从缓冲区了拿数据即可,这样就不会因为彼此的处理速度而发生阻塞。
接上面的例子,如果我们不使用邮筒,我们就得在邮局等邮递员,直到他回来,我们把信件交给他,这期间我们啥事儿都不能干(也就是生产者阻塞),或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询)。
3、支持忙闲不均
缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。 等生产者的制造速度慢下来,消费者再慢慢处理掉。
为了充分复用,再拿寄信的例子来说事。假设邮递员一次只能带走1000封信。万一某次碰上情人节(也可能是圣诞节)送贺卡,需要寄出去的信超过1000封,这时 候邮筒这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来 时再拿走。
对生产者与消费者模型的阐释就进行到这里,用代码实现生产者与消费者模型
包子工厂
import threading, time, queue
q = queue.Queue()
def consumer(q):
while True:
msg = q.get()
if isinstance(msg, str) and msg == "quit":
break
else:
print(msg)
print("Bye byes")
def producer():
start_time = time.time()
while time.time() - start_time < 5:
q.put('something at %s' % time.time())
time.sleep(1)
q.put('quit')
factory =threading.Thread(target=producer)
worker = threading.Thread(target=consumer, args=(q,))
factory.start() # 开启生产者线程
worker.start() # 开启消费者线程
协程
在学习异步IO模型前,先来了解协程。
一大波阐释即将到临,非高能请注意闪躲(仔细阅读)
概念: 协程,又称微线程,纤程。英文名Coroutine。 是非抢占式的程序 主要也是解决I/O操作的
协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。
子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
优点:
优点1: 协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
优点2: 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
在此引申了下生成器的内容
# 生成器
def f():
print("ok")
s = yield 6
print(s)
print("ok2")
yield
gen=f()
# print(gen)
# next(gen) # 方法一
# next(gen)
RET=gen.__next__() # 方法二
print(RET)
gen.send(5) # 方法三
import time
import queue
def consumer(name):
print("--->ready to eat baozi........")
while True:
new_baozi = yield # yield实现上下文切换,传包子进来
print("[%s] is eating baozi %s" % (name,new_baozi))
#time.sleep(1)
def producer():
r = con.__next__()
r = con2.__next__()
n = 0
while 1:
time.sleep(1)
print("\033[32;1m[producer]\033[0m is making baozi %s and %s" %(n,n+1) )
con.send(n) # 发送告诉他有包子了
con2.send(n+1)
n +=2
if __name__ == '__main__':
con = consumer("c1")
con2 = consumer("c2")
producer()
greenlet是一个用C实现的协程模块,相比与python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator
from greenlet import greenlet
def test1():
print(12)
gr2.switch()
print(34)
gr2.switch()
def test2():
print(56)
gr1.switch()
print(78)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
gr2.switch()
Gevent
import gevent
import requests,time
start_time = time.time()
def get_url(url):
print("get: {}".format(url))
resp = requests.get(url)
data = resp.text
print(len(data),url)
# get_url('https://www.python.org/')
# get_url('https://www.yahoo.com/')
# get_url('https://www.baidu.com/')
# get_url('https://www.sina.com.cn/')
# get_url('http://www.xiaohuar.com/')
gevent.joinall(
[
gevent.spawn(get_url, 'https://www.python.org/'),
gevent.spawn(get_url, 'https://www.yahoo.com/'),
gevent.spawn(get_url, 'https://www.baidu.com/'),
gevent.spawn(get_url, 'https://www.sina.com.cn/'),
gevent.spawn(get_url,'http://www.xiaohuar.com/')
]
)
print(time.time()-start_time)
协程的优势 :
1、没有切换的消耗
2、没有锁的概念
有一个问题:能用多核吗?
答:可以采用多进程+协程,是一个很好的解决并发的方案
另外 如果有返回值,可以x = gevent.jionall([])接受,返回列表 可以用x[0].value获取返回值