This PEP proposes some enhancements to the API and syntax of generators, to make them usable as simple coroutines.

                                                                                                                                             ——— Pep342

博文 Iterator, Generator 与 Yield 介绍了 iterator,generator 和 yield,并阐述三者之间的关系。本文将进一步介绍 yield 和 coroutine,并阐述如何通过 yield 实现一个简单的 coroutine。

Coroutine(协程) 最早于 1963 年提出,在之后的三四十年里并没有受到广泛关注,但在近些年受到热捧。通俗而言:协程相当于用户态线程。这句话包含了两层意思:

  • 协程具有类似线程的功能,它能提供并发。
  • 协程是用户态的,即操作系统对协程不感知,也不负责调度;应用程序负责管理协程的生命周期和调度协程,由于协程的切换是函数级别的切换,故切换的开销远远小于线程/进程。

为什么协程在近些年越来越火?原因得从并发谈起,随着互联网爆炸式增长,服务端对并发能力的需求越来越大。最初工程师采用多进程提供并发,每当服务端收到一个请求,就 fork 一个进程处理请求,Apache 是最典型的例子。但是进程很重,占用了的大量 CPU 和内存资源,和进程相比,线程占用更少的资源,所以多线程的并发模型更受欢迎,每当服务端收到一个请求,就创建一个线程处理请求,如 Nginx。每个线程维护私有的 stack,Linux 下 stack 的默认大小为 8MB,所以 8G 内存的 Linux 服务器最多能创建 1000 个线程;此外线程的调度由内核负责,调度和切换的开销也不容小觑,以上两个因素限制了多线程模型的并发能力。

为了提升服务端的并发能力,我们需要一种并发模型,这种模型具有以下特点:

  • 占用更少的资源,如内存和 CPU 周期
  • 避免内核调度带来的额外开销,协程仅在必要的时候才被调度切换,如 IO 操作。

而协程作为用户态线程,恰好满足上述要求,首先协程的 stack 比线程的 stack 更小,占用更少的内存空间(KB 级别),无需通过系统调用来创建和销毁,故消耗更少的 CPU 周期;另外,协程的调度由应用程序负责,仅在必要的时候才切换,和线程相比,减少了切换的次数和每次切换的开销。

回顾 Yield,我们用它编写一个如下的 Generator。

def task1():
    while True:
        # Do something
        yield "This is task1"

运行:

>>> t1 = task1()              # Initiate a task
>>> t1.next()                 # Run task
>>> This is task1             # Task suspend when meeting next yield
>>> t1.Send(None)             # Run task
>>> This is task1             # Task suspend when meeting next yield
>>> t1.close()                # Delete task

当我们调用一个包含 yield 的函数时,相当于初始化了一个协程,我们可以调用 next() 或者 send() 让协程运行,每当 task 遇到 yield 则自动暂停挂起,我们也可以调用 close() 结束一个协程。以下是进程,线程和上例协程生命周期管理的部分函数。

Method          Process         Thread           Coroutine
----------------------------------------------------------------------
Create/Run      fork            pthread_create   task1()/next/send
Delete          exit            pthread_exit     close

上例协程具备基本的生命周期管理的方法,我们现在往其加入调度功能,调度算法为 FIFO。

class Scheduler(object):
    def __init__(self):
        self.queue = []
        self.task_num = 0

    def new(self, task):
        self.queue.append(task)
        self.task_num += 1

    def loop(self):
        while self.task_num:
            task = self.queue.pop(0)
            task.next()
            self.queue.append(task)


def task2():
    while True:
        # Do something
        yield "This is task2"

运行结果:

>>> scheduler = Scheduler()
>>> scheduler.new(task1())
>>> scheduler.new(task2())
>>> scheduler.loop()
This is task1
This is task2
This is task1
This is task2
......

流程图如下,每当 Main Loop 调用 next()/send(),执行权交给相应协程,每当协程遇到 yield,则交出执行权给 Main Loop。该流程图和 Linux 的进程调度非常类似,如果把 Main Loop 比作内核,协程如同进程/线程,yield 如同系统调用、硬件中断等。任何时刻,一个 Python 进程内只有一个协程在执行,即协程是伪并发的。

           Run       Run      Run      Run      Run
Main Loop  --->      -->      -->      -->      --->
               |    |   |    |   |    |   |    |
               |Run |   |    |   |Run |   |    |     ......
Task1           --->    |    |    --->    |    |
                        |Run |            |Run |
Task2                    --->              --->

本文借用 yield 实现了一个非常简单的协程,它的调度功能非常简陋,不支持同步功能,没有处理阻塞的 IO,无法处理复杂事务,另推荐 A Curious Course on Coroutines and Concurrency 和 Python 协程库 eventletgevent 做进一步的学习。