并发编程之协程
协程
什么是协程
在单个线程下实现并发效果,在多个任务之间切换。协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置,当程序中存在大量不需要CPU的操作时(IO),适用于协程。
官方说法:协程称为微线程,就是操作系统级别的线程。是由操作系统来控制调度的。
线程出现的问题
-
GIL锁 导致多线程无法并行执行,只能并发执行,效率低。但是并发时我们要实现的最终目的(最好并行)
-
线程出现假死状态
-
例如tcp服务器,限制了最大线程数量1000,如果第1000个客户有一部分,没有进行任何的操作,而新任务将无法被处理,即使CPU空闲
使用协程的好处
-
协程有极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销
-
不需要多线程的锁机制,因为只有一个线程,不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了。所以执行效率比多线程高很多。
-
因为协程是一个线程执行,所以想要利用多核CPU,最简单的方法是多进程+协程,这样既充分利用多核,又充分发挥协程的高效率。
符合什么条件就能称之为协程:
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 一个协程遇到IO操作自动切换到其它协程
协程的使用场景
- IO密集型任务,且任务数量非常多。
Python中对于协程有两个模块,greenlet和gevent。
Greenlet(greenlet的执行顺序需要我们手动控制)
第1阶段:无作为
import greenlet
def task1():
print("task1 start")
time.sleep(2)
print("task1 over")
def task2():
print("task2 start")
time.sleep(2)
print("task2 over")
g1 = greenlet.greenlet(task1)
g2 = greenlet.greenlet(task2)
start_time = time.time()
g1.switch() # 可以理解为:开始g1协程,执行完毕后,开始g2协程
g2.switch() # 开始g2协程
end_time = time.time() - start_time
print(end_time)
task1 start
task1 over
task2 start
task2 over
4.000658273696899
不作为的情况下,就是串行。
第2阶段:面对IO操作,手动切换协程(可以理解为并发)
import greenlet
def task1():
print("task1 start")
g2.switch() # 下面即将面对IO操作。于是切换到g2协程
time.sleep(2)
print("task1 over")
g2.switch() # g1协程执行完毕,切换到g2协程
def task2():
print("task2 start")
g1.switch() # 下面即将面对IO操作。于是切换到g1协程
time.sleep(2)
print("task2 over")
g1 = greenlet.greenlet(task1)
g2 = greenlet.greenlet(task2)
# 计算时间
start_time = time.time()
g1.switch() # 开启g1协程.这里注意g2协程已经在g1协程中开启了。所有在外面无需再开了
end_time = time.time() - start_time
print(end_time)
task1 start
task2 start
task1 over
task2 over
4.000972509384155
第3阶段:面对计算操作,手动切换协程(可以理解为并发,但是相对串行更复杂了)
import greenlet
def task1():
print("task1 start")
g2.switch()
for i in range(50000000):
1 + 1
print("task1 over")
g2.switch()
def task2():
print("task2 start")
g1.switch()
for i in range(50000000):
1 + 1
print("task2 over")
g1 = greenlet.greenlet(task1)
g2 = greenlet.greenlet(task2)
# 计算时间
start_time = time.time()
g1.switch()
end_time = time.time() - start_time
print(end_time)
task1 start
task2 start
task1 over
task2 over
2.6845099925994873
第4阶段:面对计算操作,不切换协程(可以理解为串行)
import greenlet
def task1():
print("task1 start")
for i in range(50000000):
1 + 1
print("task1 over")
def task2():
print("task2 start")
for i in range(50000000):
1 + 1
print("task2 over")
g1 = greenlet.greenlet(task1)
g2 = greenlet.greenlet(task2)
# 计算时间
start_time = time.time()
g1.switch()
g2.switch()
end_time = time.time() - start_time
print(end_time)
task1 start
task2 start
task1 over
task2 over
2.6445099925994873
总结:
- 需要手动切换协程,增加了逻辑复杂度
- 效率不高,跟串行的效率相差不大
gevent(自动切换,由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成)
第1阶段:gevent对于io操作的处理方式(并发)
from gevent.monkey import patch_all # 猴子补丁。geven实现并发的原理,是将原本阻塞的代码变为非阻塞
import gevent
import time
patch_all() # 对导入的模块进行打补丁
def task1():
print("task1 start")
time.sleep(2) # 遇到阻塞操作,切换到另外一个任务
print("task1 over")
def task2():
print("task2 start")
time.sleep(2) # 遇到阻塞操作,切换到另外一个任务
print("task2 over")
g1 = gevent.spawn(task1)
g2 = gevent.spawn(task2)
# 计算时间
start_time = time.time()
g1.join() # 开启一个 就能让所有的协程进行工作
end_time = time.time() - start_time
print(end_time)
task1 start
task2 start
task1 over
task2 over
2.003842830657959
第2阶段:gevent对于计算操作的处理方式(串行)
from gevent.monkey import patch_all
import gevent
import time
patch_all()
def task1():
print("task1 start")
for i in range(50000000): # 此时遇到的不是IO操作,而是计算,因此协程不会切换,而是直接执行完
1 + 1
print("task1 over")
def task2():
print("task2 start")
for i in range(50000000):
1 + 1 # 此时遇到的不是IO操作,而是计算,因此协程不会切换,而是直接执行完
print("task2 over")
g1 = gevent.spawn(task1)
g2 = gevent.spawn(task2)
# 计算时间
start_time = time.time()
g1.join() # 开启一个 就能让所有的协程进行工作
end_time = time.time() - start_time
print(end_time)
task1 start
task1 over
task2 start
task2 over
2.5767011642456055
总结:
- 对于不同任务,采取的运行方式不同
- 对于IO密集型操作,gevent模块采用的是并发的方式。即当协程中有IO操作(阻塞状态)时,任务立即切换到另外一个任务。
- 对于计算密集型操作,gevent模块采用的是串行的方式。计算属于运行状态,因此协程不能切换。所以阶段二是串行的
- 与greenlet模块,优点:
- 第一:gevent更智能。gevent模块遇到IO操作,自动切换协程。greenlet模块则需要手动切换
- 第二:gevent更高效。对于不同阶段的时间可以看出,不论是IO密集型还是计算密集型,gevent消耗的时间更少
- 第三:gevent简化了操作。不需要人为手动切换协程
因此,协程使用gevent模块更好!