由于GIL的存在,导致Python多线程性能甚至比单线程更糟。
协程: 协程,又称微线程,纤程,英文名Coroutine。协程的作用,是在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行.
协程由于由程序主动控制切换,没有线程切换的开销,所以执行效率极高。对于IO密集型任务非常适用,如果是cpu密集型,推荐多进程+协程的方式。
- 写一个·简单的协程
def run1():
print('run1---task1')
yield
print('run1---task2')
yield
print('run1---task3')
yield
print('run1---task4')
yield
print('run1---task5')
yield
def run2():
print('run2---task1')
yield
print('run2---task2')
yield
print('run2---task3')
yield
print('run2---task4')
yield
print('run2---task5')
yield
def main():
print('main---task')
r1 = run1()
r2 = run2()
while True:
try:
next(r1)
next(r2)
except Exception:
print('所有任务运行完毕')
break
if __name__ == '__main__':
main()
协程,其实就是伪多线程,利用生成器yield会暂停一下的原理,一个函数执行一块,另一个函数再执行,到yield关键字就暂停换别的函数执行,这和多线程是很相似的。
- 协程执行过程
协程中yield关键字通常出现在等号的右边,比如c = yield a + b。在赋值语句中,等号右边的代码在赋值之前执行。因此,协程首先会执行yield a + b,产出表达式a + b的值,然后协程会在yield关键字所在的位置暂停(suspend)。等到调用方执行.send(10)时,协程会从之前暂停的地方恢复执行(resume),而且.send(10) 方法的参数10会成为暂停的yield表达式的值。所以yield a + b整体等于10,然后再赋值给变量c:
def run(a):
print(a)
b = yield a
print(b)
c = yield b
print(c)
yield c
r = run(1)
next(r) #预激协程
r.send(2)
r.send(3)
# 输出结果
# 1
# 2
# 3
- 预激协程的装饰器
import functools
def active(func):
@functools.wraps(func)
def real_run(*args,**kwargs):
r = func(*args,**kwargs)
next(r)
return r
return real_run
@active
def run(a):
print(a)
b = yield a
print(b)
c = yield b
print(c)
yield c
r = run(1)
r.send(2)
r.send(3)
# 输出结果
# 1
# 2
# 3
- close()和throw()方法是用来终止协程的,由于协程遇到异常就会终止,所以我们终止协程的方法就是向协程发送异常,但是要保证这个异常只会让协程结束,而不会让全部程序结束
- 所以这暗示了一种终止协程的方法:发送某种错误,让协程退出,
generator.throw(exc_type[, exc_value[, traceback]])
如果协程中没有处理异常的语句,则异常往上冒泡- 另一种终止协程则是抛出GeneratorExit异常,即:
generator.close()
throw方法向协程抛出异常
def run():
a = 0
while True:
try:
a = yield a
print(a)
except TypeError:
print('出现异常')
r = run()
next(r) #预激协程
r.send(1)
r.throw(TypeError,'throw a erro')#向协程中抛出异常
r.send(2)
r.send(3)
# 输出结果
# 1
# 出现异常
# 2
# 3
向协程中抛出异常。并在协程中处理,若传入协程中的异常没有处理,那么协程就会关闭。
用close关闭协程之后,对协程的任何操作,例如send(),next()都将失效,若强制使用,就会报错
def run():
a = 0
while True:
try:
a = yield a
print(a)
except TypeError:
print('出现异常')
r = run()
next(r) #预激协程
r.send(1)
r.close()#关闭协程
r.send(2)
r.send(3)
# 输出结果
# 1
# 报错StopIteration
- 让协程返回值
def run(a):
print(a)
b = yield a
print(b)
c = yield b
print(c)
yield c
return a + b + c
r = run(1)
next(r) #预激协程
r.send(2)
r.send(3)
result = r.send(None)
print('reuslt = ',result)
# 输出结果
# 1
# 2
# 3
# 报错StopIteration: 6
- 从结果我们可以看到,返回值在异常里,接下来我们从异常中获取这个值,只需要捕获这个异常,获得其中的值
try:
result = r.send(None)
except StopIteration as exc:
print('reuslt = ',exc.value)
# 输出结果
# 1
# 2
# 3
# 报错StopIteration: 6
- 生产者消费者模式(生产一个,消费一个)
import random
import time
def producer(a):
next(a)
while True:
item = random.randint(0,99)
print('生产者生产产品',item)
time.sleep(1)
a.send(item)
def consumer():
while True:
product = yield
print('消费者消费',product)
time.sleep(1)
producer(consumer())
# 输出结果
# 生产者生产产品 39
# 消费者消费 39
# 生产者生产产品 16
# 消费者消费 16
# 生产者生产产品 13
# 消费者消费 13
# 生产者生产产品 51
# 消费者消费 51
# 生产者生产产品 92
# 消费者消费 92
# 生产者生产产品 31
# 消费者消费 31
- greenlet协程
由来 虽然CPython(标准Python)能够通过生成器来实现协程,但使用起来还并不是很方便。 与此同时,Python的一个衍生版 Stackless Python。实现了原生的协程,它更利于使用。 于是,大家开始将 Stackless 中关于协程的代码 。单独拿出来做成了CPython的扩展包。 这就是 greenlet 的由来,因此 greenlet 是底层实现了原生协程的 C扩展库。
import greenlet
import random
import time
def producer():
while True:
item = random.randint(0,99)
print('生产者生产产品',item)
time.sleep(1)
c.switch(item)# 将item转给c并跳转到c
def consumer():
while True:
product = p.switch()
print('消费者消费',product)
time.sleep(1)
c = greenlet.greenlet(consumer)# 创建协程
p = greenlet.greenlet(producer)
c.switch()# c先执行
# 输出结果
# 生产者生产产品 23
# 消费者消费 23
# 生产者生产产品 27
# 消费者消费 27
# 生产者生产产品 37
# 消费者消费 37
# 生产者生产产品 98
# 消费者消费 98
# 生产者生产产品 47
# 消费者消费 47
# 生产者生产产品 3
# 消费者消费 3
# 生产者生产产品 10
# 消费者消费 10
但是greenlet还是要手动跳转协程,所以之后有了gevent协程
- gevent协程
gevent协程主要实现了自动跳转协程,当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
import gevent
def run(n):
for i in range(n):
print(gevent.getcurrent(),i)
g1 = gevent.spawn(run,5)
g2 = gevent.spawn(run,5)
g3 = gevent.spawn(run,5)
g1.join()# 等待g1执行完毕,相当于是一个耗时的事件
g2.join()
g3.join()
# 输出结果
# <Greenlet at 0x16337dff260: run(5)> 0
# <Greenlet at 0x16337dff260: run(5)> 1
# <Greenlet at 0x16337dff260: run(5)> 2
# <Greenlet at 0x16337dff260: run(5)> 3
# <Greenlet at 0x16337dff260: run(5)> 4
# <Greenlet at 0x16337dff6a0: run(5)> 0
# <Greenlet at 0x16337dff6a0: run(5)> 1
# <Greenlet at 0x16337dff6a0: run(5)> 2
# <Greenlet at 0x16337dff6a0: run(5)> 3
# <Greenlet at 0x16337dff6a0: run(5)> 4
# <Greenlet at 0x16337dff590: run(5)> 0
# <Greenlet at 0x16337dff590: run(5)> 1
# <Greenlet at 0x16337dff590: run(5)> 2
# <Greenlet at 0x16337dff590: run(5)> 3
# <Greenlet at 0x16337dff590: run(5)> 4
# <Greenlet at 0x16337dff7b0: run(5)> 0
# <Greenlet at 0x16337dff7b0: run(5)> 1
# <Greenlet at 0x16337dff7b0: run(5)> 2
# <Greenlet at 0x16337dff7b0: run(5)> 3
# <Greenlet at 0x16337dff7b0: run(5)> 4
# <Greenlet at 0x16337dff8c0: run(5)> 0
# <Greenlet at 0x16337dff8c0: run(5)> 1
# <Greenlet at 0x16337dff8c0: run(5)> 2
# <Greenlet at 0x16337dff8c0: run(5)> 3
# <Greenlet at 0x16337dff8c0: run(5)> 4
# <Greenlet at 0x16337dff9d0: run(5)> 0
# <Greenlet at 0x16337dff9d0: run(5)> 1
# <Greenlet at 0x16337dff9d0: run(5)> 2
# <Greenlet at 0x16337dff9d0: run(5)> 3
# <Greenlet at 0x16337dff9d0: run(5)> 4
分析一下这段代码,我们用gevent.spawn()创建了三个协程,三个协程被创建的一瞬间就能够开始执行,但是并没有执行。当调用到g1.jion()表示等待g1执行结束,这是一个很耗时间的过程,所以系统就会检测我们之前创建过的协程,发现有g1协程可以执行,就开始执行g1,以此类推。
但是这个运行结果并不是我们想要的,因为它不像是并发,是单线程的,所以我们要通过修改代码来实现。
import gevent
def run(n):
for i in range(n):
print(gevent.getcurrent(),i)
gevent.sleep(0.5)# 耗时语句
g1 = gevent.spawn(run,5)
g2 = gevent.spawn(run,5)
g3 = gevent.spawn(run,5)
g1.join()# 等待g1执行完毕,相当于是一个耗时的事件
g2.join()
g3.join()
# 输出结果
# <Greenlet at 0x1633627ce10: run(5)> 0
# <Greenlet at 0x16337dff590: run(5)> 0
# <Greenlet at 0x16337dff6a0: run(5)> 0
# <Greenlet at 0x1633627ce10: run(5)> 1
# <Greenlet at 0x16337dff590: run(5)> 1
# <Greenlet at 0x16337dff6a0: run(5)> 1
# <Greenlet at 0x1633627ce10: run(5)> 2
# <Greenlet at 0x16337dff590: run(5)> 2
# <Greenlet at 0x16337dff6a0: run(5)> 2
# <Greenlet at 0x1633627ce10: run(5)> 3
# <Greenlet at 0x16337dff590: run(5)> 3
# <Greenlet at 0x16337dff6a0: run(5)> 3
# <Greenlet at 0x1633627ce10: run(5)> 4
# <Greenlet at 0x16337dff590: run(5)> 4
# <Greenlet at 0x16337dff6a0: run(5)> 4
我们加上了一个gevent.sleep(),就能实现并发。
当g1运行到sleep()的时候,系统会寻找其它的协程,找到g2,g2也会运行的sleep(),而这个时候g1的sleep()并没有结束,所以进入到g3,之后发现没有协程可以选择,就等待g1,继续执行。
若要执行阻塞时间实现协程转换,就必须把所有的阻塞全部换成gevent的,例如gevent.sleep(),但是用time.sleep()就不行。这样就很麻烦,所以我们可以打个补丁
from gevent import monkey
monkey.patch_all()
加上这句话,我们用阻塞操作就可以不用gevent专有的了,有了这个补丁,系统自动把程序的所有阻塞事件换成gevent特有的
协程的真正意义:把延迟的时间利用起来,去做别的事情,提高效率。