并发的本质: 切换+保存状态
概念
协程是单线程下的并发,又称微线程,纤程。英文名Coroutine。是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)。为啥说它是一个执行单元,因为它自带CPU上下文。这样只要在合适的时机,我们可以把一个协程切换到另一个协程。只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
通俗理解
在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定
协程和线程差异
在实现多任务时, 线程切换从系统层面远不止保存和恢复CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
协程的优势
协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
单线程内就可以实现并发的效果,最大限度地利用cpu
协程的缺点
- 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
- 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
协程的特点
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了 gevent模块(select机制))
协程的本质
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。
实现方式
方式一 Greenlet 模块
首先要安装Greenlet模块 pip install greenlet
import time
from greenlet import greenlet
def study(name):
print(f"{name} 正在学习面向对象...")
time.sleep(2)
g2.switch("qiaoba")
print(f"{name} 正在学习协程...")
g2.switch("qiaoba")
def play(name):
print(f"{name} 正在玩超级玛丽...")
time.sleep(2)
g1.switch("lufei")
print(f"{name} 正在玩CS...")
g1 = greenlet(study) # xxx = greenlet(fun方法名)
g2 = greenlet(play)
g1.switch("lufei") # 使用xxx.switch("参数") 手动切换另一个方法
缺点是遇到IO阻塞不会自动切换,需要人工切换,比较麻烦
方式二 Gevent模块
python还有一个比greenlet更强大的并且能够自动切换任务的模块 gevent
原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
首先要安装Gevent模块 pip3 install gevent
用法
g1 = gevent.spawn(fun方法名,args参数1,x=args参数2...) # 创建一个协程对象g1,参数是传给方法的位置实参或关键字实参
g1.join() # 等待g1结束
gevent.joinall([g1,g2]) # 等待g1g2全部结束
g1.value # 获得g1的返回值
原来的gevent不能识别原程序中的耗时操作的代码,必须要替换为gevent自带的方法。
可以通过 打补丁 的方式将程序中用到的耗时操作的代码,自动替换为gevent中自己实现的模块,这样例如time.sleep()等耗时操作gevent便可以识别了
from gevent import monkey
monkey.patch_all() # 自动替换全部耗时操作的代码
方式三(模拟) yield
import time
def consumer():
while 1:
x = yield 0
print(x)
def producer():
g = consumer()
g.__next__()
for i in range(100):
g.send(i)
print(i)
start = time.time()
producer()
print(time.time()-start) # 0.0010056495666503906
start = time.time()
for i in range(100):
print(i)
print(time.time()-start) # 0.0010077953338623047
方法四 asyncio模块(已过时)
在Python3.4之前官方未提供协程的类库,一般大家都是使用greenlet等其 他来实现。在Python3.4发布后官方正式支持协程,即:asyncio模块。
import asyncio
@asyncio.coroutine
def func1():
print(1)
yield from asyncio.sleep(2) # 遇到IO耗时操作,自动化切换到tasks中的其他任务
print(2)
@asyncio.coroutine
def func2():
print(3)
yield from asyncio.sleep(2) # 遇到IO耗时操作,自动化切换到tasks中的其他任务
print(4)
tasks = [
asyncio.ensure_future( func1() ),
asyncio.ensure_future( func2() )
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
方法五 async & awit 模块
async & awit 关键字在Python3.5版本中正式引入,基于他编写的协程代码 其实就是 上一示例 的加强版,让代码可以更加简便。Python3.8之后@asyncio.coroutine 装饰器就会被移除,推荐使用 async & awit 关键字实现协程代码。
import asyncio
async def func1():
print(1)
await asyncio.sleep(2)
print(2)
async def func2():
print(3)
await asyncio.sleep(2)
print(4)
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
进程、线程和协程的简单总结
- 进程是资源分配的单位
- 线程是操作系统调度的单位
- 进程切换需要的资源很最大,效率很低
- 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
- 协程切换任务资源很小,效率高
- 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发