先理解什么是协程: 1,协程又称微线程,它的上下文切换不是由cpu进行控制。 2,一个线程中可以包含多个协程,对cpu而言,并不存在协程这个概念。 3,通俗来说,协程就是协同多任务。 4,协程拥有自己的寄存器上下文和栈,协程调度切换到其他协程时,将寄存器上下文和栈保存,在切回到当前协程的时候,恢复先前保存的寄存器上下文和栈。
协程有什么优点? 1,无需负担上下文切换的开销。 2,不需要加锁。 3,程序员切换控制流,方便编程。 4,高并发、高扩展、低成本(一个CPU支持上万的协程都不是问题)。
协程有什么缺点? 1,协程自己无法利用CPU多核资源(除非与多进程或者多线程配合); 2,遇到阻塞操作会使整个程序阻塞。
python中的生成器关键字yield其实就可以简单的模拟实现协程的功能
import time
def func1():
print('任务1开始')
tem = 0
for i in range(10):
print('yield上方的tem:',tem)
print('任务1暂停')
print('当前i值:',i)
tem = yield i #生成i值,切换到func2函数
print('任务1继续')
print('yield下方的tem:',tem) #当i为偶数时,这时候的send还未触发,tem = yield i 的返回值为None
time.sleep(1)
def func2():
print('任务2开始')
for i in range(6):
print('任务二执行')
print(next(func)) #切换到func1函数执行
func.send(i+10) #给右边的tem赋值
if __name__ == '__main__':
func = func1()
func2()
结果
任务2开始
任务二执行
任务1开始
yield上方的tem: 0
任务1暂停
当前i值: 0
0
任务1继续
yield下方的tem: 10
yield上方的tem: 10
任务1暂停
当前i值: 1
任务二执行
任务1继续
yield下方的tem: None
yield上方的tem: None
任务1暂停
当前i值: 2
2
任务1继续
yield下方的tem: 11
yield上方的tem: 11
任务1暂停
当前i值: 3
任务二执行
任务1继续
yield下方的tem: None
yield上方的tem: None
任务1暂停
当前i值: 4
4
任务1继续
yield下方的tem: 12
yield上方的tem: 12
任务1暂停
当前i值: 5
任务二执行
任务1继续
yield下方的tem: None
yield上方的tem: None
任务1暂停
当前i值: 6
6
任务1继续
yield下方的tem: 13
yield上方的tem: 13
任务1暂停
当前i值: 7
任务二执行
任务1继续
yield下方的tem: None
yield上方的tem: None
任务1暂停
当前i值: 8
8
任务1继续
yield下方的tem: 14
yield上方的tem: 14
任务1暂停
当前i值: 9
任务二执行
任务1继续
yield下方的tem: None
Traceback (most recent call last):
File "C:/Users/Administrator/PycharmProjects/review/python知识复习/02/gevent协程.py", line 27, in
func2()
File "C:/Users/Administrator/PycharmProjects/review/python知识复习/02/gevent协程.py", line 22, in func2
print(next(func))
StopIteration
可以看到,这个yield切换是有缺陷的,中间会跳过一次生成出来的i值。 最后的报错是我故意的,生成器取不到值了就会报StopIteration的错误,你把func2中的range(6)改成5及以下就行了。
这样模拟协程的使用有点笨重和傻傻的,其实关于协程python有很多已经封装好的第三方模块可以使用
greenlet greenlet是用c语言写的用来实现协程的第三方python模块,它可以实现多任务之间的随意切换,并不需要像上方的案例一样,先把函数变成一个生成器
import greenlet
def func1():
for i in range(6):
print(i)
g2.switch() #切换到func2
def func2():
for i in range(6,12):
print(i)
g1.switch() #切换到func1
g1 = greenlet.greenlet(func1)
g2 = greenlet.greenlet(func2)
g1.switch()
结果
0
6
1
7
2
8
3
9
4
10
5
11
上面这段代码有一个缺点,就是任务的切换都是我们手动调用的,比如你在switch前面加一个time.sleep(),它就会等待sleep完再往下执行,并不会自动切换 那么如何让任务遇到I/O操作就切换?
import gevent
def func1():
print('任务1')
gevent.sleep(2) #遇到等待操作切换到func2,gevent.sleep是gevent封装的非阻塞模块
print('任务1go')
def func2():
print('任务2')
gevent.sleep(2) #这里又切换回func1
print("任务2go")
start = time.time()
gevent.joinall([gevent.spawn(func1),
gevent.spawn(func2)])
print(time.time()-start)
结果
任务1
任务2
任务1go
任务2go
2.018115520477295
如果我改一下代码:
import gevent
def func1():
print('任务1')
time.sleep(2)
print('任务1go')
def func2():
print('任务2')
time.sleep(2)
print("任务2go")
start = time.time()
gevent.joinall([gevent.spawn(func1),
gevent.spawn(func2)])
print(time.time()-start)
结果
任务1
任务1go
任务2
任务2go
4.018229961395264
现在就没有进行异步调用了,也就是说,遇到了IO操作,并没有实现任务切换的效果了,这是为什么呢?在python的标准库的许多模块当中,都是阻塞式系统调用,time.sleep()是系统模块,gevent.sleep()是gevent封装好的非阻塞时间模块,所以用gevent.sleep()的时候协程就会自动进行任务切换。 那么系统模块的功能这么丰富,就想用系统模块怎么办呢? Monkey-patching猴子补丁就出来了,用了猴子补丁,这些 socket、ssl、threading和 select、time、multiprocessing等模块都会被修改成支持gevent功能的模块,然后就可以愉快的使用了
import gevent
from gevent import monkey
monkey.patch_all()
def func1():
print('任务1')
time.sleep(2)
print('任务1go')
def func2():
print('任务2')
time.sleep(2)
print("任务2go")
start = time.time()
gevent.joinall([gevent.spawn(func1),
gevent.spawn(func2)])
print(time.time()-start)
结果
任务1
任务2
任务1go
任务2go
2.012115001678467
注意,猴子补丁尽量早点打,否则可能出现一些奇怪的错误。