8.8 协程
我们都知道线程间的任务切换是由操作系统来控制的,而协程的出现,就是为了减少操作系统的开销,由协程来自己控制任务的切换
协程本质上就是线程。既然能够切换任务,所以线程有两个最基本的功能:一是保存状态;二是任务切换
8.8.1 协程的特点
【优点】
- 线程任务切换开销小,属于程序级的切换,操作系统感知不到
- 单线程内就可以实现并发的效果,最大限度的利用CPU
【缺点】
- 协程的本质是单线程,无法利用多核;可以一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
- 协程是单个线程执行多个任务,一旦协程遇到阻塞,将会阻塞整个线程
【特点】
- 必须要在单线程中实现并发
- 修改共享数据不需要加锁
- 用户程序内保存多个控制流的上下文栈
- 一个协程遇到IO操作自动切换到其它协程
8.8.2 Greenlet
使用grennlet第三方库实现任务间的切换
from greenlet import greenlet
def get_money(name):
print(f"{name} get 10 $")
g2.switch('jiawen')
print(f"{name} get 20 $")
g2.switch()
def buy_goods(name):
print(f"{name} buy no.1 good ")
g1.switch()
print(f"{name} buy no.2 good ")
g1.switch()
if __name__ == '__main__':
g1 = greenlet(get_money)
g2 = greenlet(buy_goods)
g1.switch('gailun') # switch在第一次时必须要传入参数,以后就不需要了
效率对比
from greenlet import greenlet
import time
def f1():
re = 1
for i in range(10000000):
re *= i
g2.switch()
def f2():
re = 1
for i in range(10000000):
re += i
g1.switch()
if __name__ == '__main__':
start = time.time()
g1 = greenlet(f1)
g2 = greenlet(f2)
g1.switch() # switch在第一次时必须要传入参数,以后就不需要了
print(f'{time.time()- start}') # 5.822627305984497
import time
def f1():
re = 1
for i in range(10000000):
re *= i
def f2():
re = 1
for i in range(10000000):
re += i
start = time.time()
f1()
f2()
print(f'{time.time()- start}') # 1.041489601135254
【结论】单纯的切换,在没有IO阻塞的情况下,协程的效率反而降低
8.8.3 Gevent介绍
Gevent也是一个第三方库,主要用来实现并发同步或是异步编程,在gevent中用到的主要模式是Greenlet, Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
【方法】
gevent.spawn(func,*args,**kwargs) spawn括号内第一个参数是函数名,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数func,spawn是异步提交任务
join() 等待调用者结束
value() 拿到调用者的返回值
遇到IO阻塞会自动切换
import gevent
def get_money(name):
print(f"{name} get 10 $")
gevent.sleep(2) #模拟的是gevent可以识别的IO阻塞
print(f"{name} get 20 $")
def buy_goods(name):
print(f"{name} buy no.1 good ")
gevent.sleep(1)
print(f"{name} buy no.2 good ")
if __name__ == '__main__':
g1 = gevent.spawn(get_money,'gailun')
g2 = gevent.spawn(buy_goods,name='jiawen')
g1.join()
g2.join()
# gevent.joinall([g1,g2])
print('__main__')
# 输出
gailun get 10 $
jiawen buy no.1 good
jiawen buy no.2 good
gailun get 20 $
__main__
【注意】如果要使gevent识别所有的io阻塞,放到被打补丁者的前面或者直接写在在文件的最开头写上以下代码
rom gevent import monkey;monkey.patch_all()
应用
# 爬虫
from gevent import monkey;monkey.patch_all()
import gevent
import requests
import time
def get_inf(url):
print(f'GET:{url}')
res = requests.get(url)
if res.status_code == 200:
print(f"{len(res.text)} get from {url}")
if __name__ == '__main__':
start_time = time.time()
gevent.joinall(
[gevent.spawn(get_inf,'https://www.python.org/'),
gevent.spawn(get_inf,'https://www.yahoo.com/'),
gevent.spawn(get_inf,'https://github.com/'),
]
)
print(f"take {time.time()-start_time} secondes")