目录
协程
什么是协程
在单个线程下实现并发效果,在多个任务之间切换。协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置,当程序中存在大量不需要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模块更好!
来源:https://www.cnblogs.com/plf-Jack/p/11153098.html