协程

五十岚2021年7月3日
大约 7 分钟

协程 Coroutine

一、协程概念:

1.1 为什么要有协程?

  • 同步编程 的并发性不高
  • 多进程编程 效率受 CPU 核数限制,当任务数量远大于 CPU 核数时,执行效率会降低(分片)
  • 多线程编程 需要线程之间的通信,而且需要锁机制来防止共享变量被不同线程乱改,有GIL(全局解释器锁) 实际上也无法做到真正的并行
  • 可不可以采用同步的方式来编写异步功能代码?
  • 能不能只用单线程就能做到不同任务间的切换?
    • 这样就没有了线程切换的时间消耗
    • 也不用使用锁机制来削弱多任务并发效率
  • 对于IO 密集型任务,可否有更高的处理方式来节省 CPU 等待时间?

产生协程,多进程和多线程是内核级别的程序,而协程是函数级别的程序,可以通过程序员自己控制,因此:

  • 仅定义一个单线程的 loop,所有 event 均在一个 loop
  • 是否需要锁机制:
  • 实现机制:事件循环 + 协程
  • 总耗时:最耗时 事件的时间
  • 应用场景:**IO 密集型 **任务

1.2 什么是协程

协程,又称微线程|纤程。英文:Coroutine 可揉挺


一句话说明:协程是一种 用户态轻量级 线程。通过一个线程,实现代码块相互切换执行,实现麻烦但效率极佳。

  • 用户态:程序员自己控制什么时候切换。利用串行,便不存在了(也不会有数据不安全的情况),因此协程好用的多。

  • 轻量级:实质上不是利用 CPU 轮询执行,而是用一个线程进行切换,无需 CPU 控制(因此协程说白了是一个单线程)


1.3 协程的优缺点

协程の好处

    1. 无需像线程一样需要上下文切换的开销

      例如:执行 100 个字节码,若是计算密集型任务,无 IO 阻塞则会进行大量的切换,CPU 资源大幅消耗

    1. 无需有原子操作的锁定同步的开销

      • 是不需要synchronized(同步)

      • 是不会被线程调度机制打断操作的

        这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (上下文切换:切换到另一个线程)

      • 原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不能被打乱,不能被切割(只执行部分)一系列操作是一个整体

    1. 方便切换控制流,简化编程模型
    1. 高并发 + 高扩展性 + 低成本:

      一个 CPU 支持上万的协程都不是问题(理论无限大)。所以很适合用于高并发处理。(因此利用的好完全可以替代线程,效率奇高)

协程の缺点

    1. 无法利用多核资源:

      协程的本质是个单线程,它不能同时将 CPU 的多核利用,协程需要和进程 配合才能运行在多 CPU 上

      • 当然我们日常所编写的绝大部分应用都没有这个必要,除非是 cpu 密集型应用。
    1. 线程阻塞 操作IO Blocking时会阻塞掉整个程序

      因此一处协程、处处协程,使用了 async/await 便所有的 IO 函数都应该使用 async/await,不然该程序的同步 IO 部分就会全局阻塞,影响性能

二、协程进化史:

实现前先给协程一个标准定义,即符合什么条件就能称之为协程

1. 必须在只有一个`单线程`里实现并发

2. 修改共享数据不需加锁

3. 用户程序里自己保存多个控制流的上下文栈

4. 一个协程遇到 IO 操作自动切换到其它协程

实现方式有以下几种:

  • 生成器 yield 关键字
  • greenlet/gevent 早期模块
  • yield from 实现
  • asyncio装饰器py3.4
  • async|await 关键字py3.5[主流实现]

2.1 yield 生成器实现

因为 yield 可以实现中断功能,所以起初,协程是用生成器来实现的,此时不是 线程级CPU 的切换,而是 执行顺序 的切换,但原理依旧

def func1():
    yield 1
    yield from func2()	# 切换到func2 执行,并保留上下文
    yield 2


def func2():
    yield 3
    yield 4


f1 = func1()

for item in f1:		# 返回了生成器
    print(item)

### 输出结果:
# 1
# 3
# 4
# 2

2.2 greenlet 实现协程

是一个用 c 实现的 协程模块,相比与python自带的yield,它能在任意函数之间随意切换,而不需把这个函数先声明为 generator|生成器

安装 greenlet

pip install greenlet
from greenlet import greenlet


def func1():
    print(1)            # 第2步:打印1
    gr2.switch()        # 第3步:切换到 func2 函数
    print(2)            # 第6步:打印2
    gr2.switch()        # 第7步:切换到 func2 函数,从上一次位置继续


def func2():
    print(3)            # 第4步:打印3
    gr1.switch()        # 第5步:切换到 func1 函数,从上一次位置继续
    print(4)            # 第8步:打印4


gr1 = greenlet(func1)
gr2 = greenlet(func2)

gr1.switch()            # 第1步:执行 func1 函数

## 输出结果:
# 1
# 3
# 2
# 4

与生成器相同,此时未实现遇见 IO 阻塞 自动切换,这在 gevent 中实现了sleep自动切换

2.3 yield from 和装饰器实现

yield from@asyncio.coroutine 是官方python 3.4之后专门提供用于实现异步I/O的模块

import asyncio


@asyncio.coroutine	# 装饰一下,变为协程函数
def func1():
    print(1)
    yield from asyncio.sleep(1)	# 当遇到IO操作时,会自动化切换到tasks中的其他任务执行
    print(2)


@asyncio.coroutine
def func2():
    print(3)
    yield from asyncio.sleep(1)	# 当遇到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))

### 输出结果
# 1
# 3
# 2
# 4

以上为基于生成器的协程,已弃用 并计划在 Python 3.10 中移除。

  • @asyncio.coroutine 装饰器是协程函数的标志,等同下文 async
  • yield from 等同下文 await

2.4 async/await 实现

把上文的 装饰器 替换为 async , yield from 替换为 await 即可

async def func1():
    print(1)
    await asyncio.sleep(1)
    print(2)


async def func2():
    print(3)
    await asyncio.sleep(1)
    print(4)

具体见异步 I/O

2.5 协程本质

[本质]
  • 协程拥有自己的 寄存器上下文 。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。
  • 函数上下文通常是自己的命名空间,而 Cpython 中上下文是用了结构体来存的,通过 *f_back 指针进行链式构成。
  • 协程则是用了一个 来定义,里面存着上下文的栈帧
上次编辑于: 2023/2/25 05:41:54