【百尺竿头,更进一步学Python】Python进阶课程——Python协程
我们都知道从编程层次而言,多任务的实现可以通过:多进程、多线程、协程来实现.
多进程和多线程在前几篇博客中我们已经进行了具体的讲解,今天我们就来讲最后一个协程(协程不是携程奥!).
协程(Coroutine)
什么是协程?
-
协程,又称微线程,纤程。
-
协程是用户级别的轻量级线程。
-
协程主解决的是IO的操作。
-
协程就是协助程序,以前我们学过的线程和进程都是抢占式特点,线程和进程的切换我们是不能参与的。
-
而协程是非抢占式特点,协程也存在着切换,这种切换是由我们用户来控制的。
-
协程就是在子程序执行的过程中,转而执行别的子程序,然后在返回来接着执行,这个过程并不是函数调用,而是中断。
-
协程拥有自己的寄存器上下文和栈。协程调度时,将寄存器上下文和栈保存到其他的地方,之后切回来时,恢复之前保存的寄存器上下文和栈继续执行。
协程的优点
- 协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
- 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
那么问题来了!协程是一个线程执行,那怎么利用多核CPU呢?
- 最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
为什么要使用协程
我们都知道Python中,我们已经有了线程和进程,为什么还要使用协程呢?
-
上面已经提到,协程是程序员写调度逻辑的,不需要CPU进行线程的切换,因此也就省下了切换上下文的开销,提升了性能。
-
同时协程不需要使用多线程中的锁机制,只有一个线程,因此在协程中控制共享资源时不需加锁,所以执行效率也比多线程高。
或许你也注意到,协程其实就是一个单线程,那么自然它也无法利用多核资源。因此在现实中,我们通常是使用多进程+协程的方法,这样既充分的利用多核的优势,又充分发挥协程的高效率,从而获得高性能。
如何使用协程?
协程的实现由很多种,在此我们介绍基本的三种方法。
(1)使用yield实现协程
def consumer(name):
print("consumer %s 要开始吃东西了"%name)
while True:
bone=yield
print("%s正在吃东西 %s"%(name,bone))
def producer(obj1,obj2):
obj1.send(None) #和obj1.__next__()等价
obj2.send(None)
n=0
while n<5:
n+=1
print("producer 正在生产食品 %s"%n)
obj1.send(n)
obj2.send(n)
if __name__=='__main__':
con1=consumer("张三")
con2=consumer("李四")
producer(con1,con2)
1234567891011121314151617181920
不明白yield的运行机制的可以看看我之前的文章。在线程博客中有详细的讲解.
(2)使用greenlet实现协程
- 为了更好的使用协程来完成多任务,Python中的greenlet模块主要就是对其进行封装,从而使得切换任务变得更加简单,使用greenlet模块可以进行手动切换。
实例:
from greenlet import greenlet
import time
def upload():
while True:
time.sleep(0.5)
print("上传文件")
grt2.switch()
def download():
while True:
time.sleep(1)
print("下载文件")
grt1.switch()
grt1=greenlet(upload)
grt2=greenlet(download)
# 控制download先执行
grt2.switch()
(3)使用gevent实现协程
- 使用greenlet模块时我们需要进行人工切换,而gevent会自动识别程序内部的IO操作,当子程序遇到IO时,它会自动切换到子程序。若所有的子程序进入到IO,则阻塞。
- 由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行而不是等待IO
安装gevent包在Linux终端命令窗口使用下面命令进行安装:
sudo apt-get install python3-gevent
实例:
import gevent
import time
def upload(num):
for i in range(num):
gevent.sleep(1)
print(f"{gevent.getcurrent()}------{i}")
def download(num):
for i in range(num):
gevent.sleep(1)
print(f"{gevent.getcurrent()}------{i}")
gvt1 = gevent.spawn(upload,5)
gvt2 = gevent.spawn(download,5)
print(time.time())
gvt1.join()
gvt2.join()
print(time.time())
用来模拟一个耗时的操作,time模块中的sleep换成gevent模块中的sleep
给程序打补丁
这个补丁就是理解为是进行替换工作
#1.导入monkey模块
from gevent import monkey
#2.然后在后面调用patch_all方法
monkey.patch_all()
#3.剩下的工作完全按照之前的开发流程
线程和协程的差异
- 在实现多任务的时候,线程切换是从系统中远不止保存和恢复CPU上下文这么简单,操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮我们做这些数据的回复操作。所以说多线程的切换非常消耗资源及计算机性能。
- 但是协程的切换只是非常单纯的操作CPU的上下文,只是切换CPU去不同的任务中从刚才停止的代码开始执行。所以协程一秒钟切换个几百万次也不是问题