08协程
一、单线程下实现并发,使用yield
并发 = 切换 + 保存状态
1、yield可以保存任务运行状态,与操作系统的保存状态很像,但是yield是代码级别控制的,更轻量级
2、send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换
单纯的切换反而会降低运行效率
# 串行执行 import time def consumer(res): '''任务1:接受数据,处理数据''' pass def producer(): '''任务2:生产数据''' res = [] for i in range(10000000): res.append(i) return res start_time = time.time() res = producer() consumer(res) stop_time = time.time() print('spend time:',stop_time - start_time)#spend time: 1.5560393333435059 # 基于yield并发执行 import time def consumer(): '''任务1:接受数据,处理数据''' while True: # print('consumer...') x = yield def producer(): '''任务2:生产数据''' g = consumer() next(g) for i in range(10000000): # print('producer...') g.send(i) start_time = time.time() # 基于yield保存状态,实现两个任务直接来回切换,即并发的效果 producer() stop_time = time.time() print('spend time:',stop_time - start_time)#spend time: 2.0483670234680176,效率低
二、协程
使用yield实现的切换并不能检测到I/O阻塞,那么这样的切换并没有意义
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的,非操作系统控制切换。
解释:
1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率
优点如下:
1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2. 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点如下:
1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程、
总结协程特点:
1.必须在只有一个单线程里实现并发
2.修改共享数据不需加锁
3.用户程序里自己保存多个控制流的上下文栈
4.附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
三、greenlet
如果我们在单个线程内有20个任务,要想实现在多个任务之间切换,使用yield生成器的方式过于麻烦
(需要先得到初始化一次的生成器,然后再调用send。。。非常麻烦),
而使用greenlet模块可以非常简单地实现这20个任务直接的切换,
但是也无法检测I/O
from greenlet import greenlet def eat(name): print('{} eat 1'.format(name)) g2.switch('cc') print('{} eat 2'.format(name)) g2.switch() def play(name): print('{} play 1'.format(name)) g1.switch() print('{} play 2'.format(name)) g1 = greenlet(eat)#不用传参数 g2 = greenlet(play) g1.switch('cc')#在第一次switch时传入参数,以后不需要
四、gevent
import gevent def eat(name): print('{} eat 1'.format(name)) gevent.sleep(3) # 遇到I/O阻塞会自动切换任务,但这里是gevent.sleep(3)模拟的识别的阻塞 print('{} eat 2'.format(name)) def play(name): print('{} play 1'.format(name)) gevent.sleep(1) print('{} play 2'.format(name)) g1 = gevent.spawn(eat, 'cc') g2 = gevent.spawn(play, 'dd') g1.join() g2.join() # 或者gevent.joinall([g1,g2])
上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,
而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了
from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前
或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头
from gevent import monkey; monkey.patch_all() import gevent import time def eat(name): print('{} eat 1'.format(name)) time.sleep(3) # 遇到I/O阻塞会自动切换任务,但这里是gevent.sleep(3)模拟的识别的阻塞 print('{} eat 2'.format(name)) def play(name): print('{} play 1'.format(name)) time.sleep(1) print('{} play 2'.format(name)) g1 = gevent.spawn(eat, 'cc') g2 = gevent.spawn(play, 'dd') g1.join() g2.join()
例1:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】