协程:单线程下的并发,又称为微线程,纤程。一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
协程是基于单线程实现并发,即只用一个主线程(cpu只用一个),为实现并发,先认识并发本质(切换+保存状态)
cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它
协程本质上就是一个线程,以前线程任务的切换是由操作系统控制的,遇到I/O自动切换,现在我们用协程的目的就是较少操作系统切换的开销
知识点:
1
2
3
4
5
|
1. 协程是基于单线程来实现并发 2. 协程的三种状态(和进程相同): 运行、阻塞、就绪 3.python 的线程属于内核级别,即由操作系统控制调度(如单线程遇到IO或执行时间过长就会交出cpu执行权限 ,切换其他线程运行) #一个线程实现并发 #优点: #多进程:操作系统控制,多个进程的多个任务切换+保持状态 |
yield本身就是一种在单线程下可以保存任务运行状态的方法:
import time def fun1(): for i in range(10): print(f"第{i}次") time.sleep(1) yield def fun2(): g = fun1() for k in range(10): next(g) #第一次next,执行到yield结束,再次yield,继续yield下面的代码 fun1() fun2() #打印结果: 第0次 第1次 第2次 第3次 第4次 第5次 第6次 第7次 第8次 第9次
# 计算密集型:串行与协程的效率对比 import time def task1(): res = 1 for i in range(1,100000): res += i def task2(): res = 1 for i in range(1,100000): res -= i start_time = time.time() task1() task2() print(time.time()-start_time) import time def task1(): res = 1 for i in range(1, 100000): res += i yield res def task2(): g = task1() res = 1 for i in range(1, 100000): res -= i next(g) start_time = time.time() task2() print(time.time() - start_time) #打印结果(可以看到:串行比协程更效率) 0.009972810745239258 0.018949270248413086
yield不能检测IO
import time def fun1(): while True: print("func1") yield def fun2(): g = fun1() for i in range(100000): next(g) time.sleep(3) print("func2") start_time = time.time() fun2() print(time.time() - start_time)
对比操作系统控制线程的切换,用户在单线程内控制协程的切换
1
2
3
4
5
6
7
8
9
10
11
|
优点: 1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级 2. 单线程内就可以实现并发的效果,最大限度地利用cpu 缺点: 1. 协程的本质是单线程下,无法利用多核。 2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程 特点: 1. 必须在只有一个单线程里实现并发 2. 修改共享数据不需加锁 3. 用户程序里自己保存多个控制流的上下文栈 4. 一个协程遇到IO操作自动切换到其它协程(如何实现检测IO, yield 、greenlet都无法实现,就用到了gevent模块(select机制)
|
二.greenlet
要想在多个任务之间进行切换,使用greenlet可以及其简单地实现。但是,greenlet只是提供了一种比yield更简单的切换方式,当切到一个任务时遇到IO,还是原地阻塞。
#版本一:切换 +保持状态(遇到IO不会主动切换) greenlet是协程的底层 from greenlet import greenlet import time def eat(name): print('%s eat 1' %name) # 2 # g2.switch('taibai') # 3 time.sleep(3) print('%s eat 2' %name) # 6 g2.switch() # 7 def play(name): print('%s play 1' %name) # 4 g1.switch() # 5 print('%s play 2' %name) # 8 g1=greenlet(eat) g2=greenlet(play) g1.switch('taibai') # 1 切换到eat任务 g1=gevent.spawn(func,1,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的,spawn是异步提交任务 g1.join() #等待g1结束 g2.join() #等待g2结束 有人测试的时候会发现,不写第二个join也能执行g2,是的,协程帮你切换执行了,但是你会发现,如果g2里面的任务执行的时间长,但是不写join的话,就不会执行完等到g2剩下的任务了 或者上述两步合作一步:gevent.joinall([g1,g2])
上图是协程真正的意义:即一旦遇到IO操作,就切换到其他地方执行,怎么搞?为了提高效率,就用到了Geven模块
三.Gevent模块
可轻松通过gevent实现并发同步或异步编程,在gevent中用到的模式主要是Greenlet
1
2
3
4
5
6
7
8
9
|
g1 = gevent.spawn(func, 1 , 3 ,x = 1 ) 创建一个协程对象,func为函数名(要执行的任务),后面是参数,spawn是异步提交任务 g1.join() g2.join() gevent.joinall([g1,g2]) 是上面 2 步的合体 g1.value #得到func的返回值 |
遇到gevent可以识别的IO阻塞就切换
import gevent def eat(name): print('%s eat 1' %name) gevent.sleep(2) print('%s eat 2' %name) def play(name): print('%s play 1' %name) gevent.sleep(1) print('%s play 2' %name) g1 = gevent.spawn(eat,"alex") #创建协程对象,同时提交任务 g2 = gevent.spawn(play,"egon") gevent.joinall([g1,g2]) #没有此代码,程序一开始就完了,因为会自动切到"主协程",主协程一完,就相当于主线程完了。。。 #执行结果: alex eat 1 egon play 1 egon play 2 alex eat 2 遇到IO就切
而如果遇到time.sleep(3)或者其他阻塞,gevent不能识别,就需要用到gevent下面的monkey模块(放在所整个py文件的最上方),此模块识别所有IO,等同于套接字.setblocking(False)
from gevent import monkey;monkey.patch_all() import gevent,time def eat(): print("eat food 1") time.sleep(2) print("eat food 2") def play(): print("play 1") time.sleep(1) print("play 2") g1 = gevent.spawn(eat) g2 = gevent.spawn(play) gevent.joinall([g1,g2]) print("__主__") #结果: eat food 1 play 1 play 2 eat food 2 __主__ *------------------------------- import time from gevent import monkey monkey.patch_all() import gevent def task(name): print(f'{name} is running') time.sleep(2) print(f'{name} is gone') def task1(name): print(f'{name} is running') time.sleep(3) print(f'{name} is gone') if __name__ == '__main__': g1=gevent.spawn(task,'立业') g2=gevent.spawn(task1,'哈哈') gevent.joinall([g1,g2]) print('主')