协程: 单线程下的并发, 又称为微线程, 纤程. 一句话说明什么是协程: 协程是一种用户态的轻量级线程, 即协程是由用户程序自己控制调度的.
协程是基于单线程实现并发, 即只用一个主线程(CPU 只用一个), 为实现并发, 先认识并发本质(切换 + 保存状态)
CPU 正在运行一个任务, 会在两种情况下切走去执行其他的任务(切换由操作系统强制控制), 一种情况是该任务发生了阻塞, 另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它
协程本质上就是一个线程, 以前线程任务的切换是由操作系统控制的, 遇到 I/O 自动切换, 现在我们用协程的目的就是较少操作系统切换的开销
知识点:
1. 协程是基于单线程来实现并发
2. 协程的三种状态(和进程相同):
运行, 阻塞, 就绪
3.python 的线程属于内核级别, 即由操作系统控制调度(如单线程遇到 IO 或执行时间过长就会交出 CPU 执行权限
, 切换其他线程运行)
- # 一个线程实现并发
- # 并发的本质:
1: 遇到 IO 阻塞, 计算密集型执行时间过长, 切换
2: 保持原来的状态
协程:
是单线程下的并发, 又称微线程, 纤程, 协程是一种用户态轻量级的线程, 协程的控制是由用户程序自己控制调度的
# 优点:
1: 协程的切换开销更小, 属于程序级别的切换, 操作系统完全感知不到, 因而更加的轻量级
2: 单线程就可以实现并发的效果, 最大限度的利用 CPU
3: 修改共享数据不用加锁
4: 一个协程遇到 IO 操作自动切换到其他协程
# 缺点:
1: 协程的本质是单线程下, 无法利用多核, 可以是一个程序开启多个进程, 每个进程开启多个线程, 每个线程开启协程
2: 协程是单个线程, 因而一旦协程出现阻塞, 将会阻塞整个线程
- # 多进程: 操作系统控制, 多个进程的多个任务切换 + 保持状态
- # 多线程: 操作系统控制. 多个线程的多个任务切换 + 保持状态
- # 协 程: 程序控制一个线程的多个任务的切换和保持状态
- # 协程: 微并发, 处理任务不易过多
协程会调度 CPU, 如果协程管控的任务中, 遇到阻塞, 他会快速的 (比操作系统快) 切换到另一个任务, 并且能将上一个任务挂起(保持状态), 让操作系统以为 CPU 一直在工作(yield 就是一个伪协程)
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 次
通过 yield 实现任务切换 + 保存装袋
- # 计算密集型: 串行与协程的效率对比
- 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. 单线程内就可以实现并发的效果, 最大限度地利用 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
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 就切
遇到 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('主')
gevent 下 monkey, 识别所有 IO 阻塞
来源: http://www.bubuko.com/infodetail-3224383.html