Skip to content

浅析协程

1423字约5分钟

Python

2020-09-20

前言

以前我们的程序都是单线程,只有一个控制流,在像Web服务这样的应用里不能同时服务多个用户。后来我们使用多线程,一个用户由一个线程全程负责,CPU根据时间片在线程之间切换,只要CPU切换得够快,用户就感受不到延迟。

但是多线程切换是由操作系统调度的,我们的应用代码无法控制。虽然说,被IO操作阻塞的线程,内核会把它挂起,不参与线程切换。但是线程也不能无限增加,否则CPU时间就花在线程切换和挂起唤醒上了,真正运行代码的时间就少了。

这时候我们就会想,多线程不就是多个控制流嘛,能不能只有一个线程,但有多个控制流,什么时候切换由我们自己决定。当然可以,这就是 协程(Coroutine)

协程和线程的区别

线程由操作系统抢占式调度,一旦开启就不会停下。而协程可以主动暂停、让出。

协程最核心的点:执行到一半的函数或程序片段能够被挂起,稍后再在挂起的地方恢复

挂起和恢复是应用程序自己控制的。所谓协程,协作式线程也。协程通过主动挂起,让出运行权来实现协作,因此当我们在讨论协程时,我们讨论的是一种程序控制流程的机制。


Python 中的协程

generator

Python 的协程是通过 生成器(generator) 来实现的。如果一个函数定义中包含 yield 关键字,那么这个函数就不再是一个普通函数,而是一个generator

generator 和 函数的区别

先来看函数

def funny():
    print(1)
    print(5)
    print(8)

k = funny() # k是 funny 调用的结果, funny 调用了

g = funny   # g 指向 funny ,funny没有调用
g()         # g() 等同于 funny(), 函数调用了

g = funny 说明 g 指向了 funny ,但没有调用, g() 才是调用。但是如果函数里有 yield 关键字,情况就不同了:

def funny():
    yield 1
    yield 5
    yield 8


g = funny()
next(g) #  1
next(g) #  5
next(g) #  8

这里 funny() 是一个 generator,并不是函数调用,所以这里 funny 并没有执行。g 是一个 generator 。执行 next(g) 会返回 yield 后面的值,下一次 next(g) 时,会从上一次 yield 的地方接着往下执行,直到又遇到 yield 又返回。

上面的 next(g) 还可以写成 g.send(None),是一样的。

def funny():
    yield 1
    yield 5
    yield 8


g = funny()
g.send(None) #  1
g.send(None) #  5
g.send(None) #  8

yield 接收参数

g.send(None) 调用 generator (在这个例子中,即 funny()),实际上,generator 还可以接收参数。

def funny():
    param = yield 5  # 第 2 步 yield 5,返回5 | 第 5 步 param = 666
    print(param) #     第 6 步 打印 666
    yield 8      #     第 7 步 yield 8


g = funny()

a = g.send(None) # 第 1 步
print(a)         # 第 3 步,打印 5

b = g.send(666)  # 第 4 步
print(b)         # 第 8 步,打印 8

第一次启动 generator 时,只能用 None 作参数。第二次就可以传参了。输出结果为:

5
666
8

执行过程如下:

  1. a = g.send(None),进入 funny()
  2. 协程执行 yield 5,返回5
  3. 主程序print(a),打印了5
  4. 主程序g.send(666)
  5. 协程从 yield 5 处继续执行,注意,yield 5 之后,不是 print(param),而是赋值语句 param =
  6. print(param),把 666 打印出来。
  7. yield 8 ,返回 8
  8. 主程序print(b),打印了8

Python协程常用范式

比如我有一个 +1 服务,每次 send 就把参数 + 1,实现如下,关键是要理解,第一次会直接 yield response,第二次开始,每次都是从 param = (send的参数) 开始。

def plus_one():
    response = 'init..'
    while True:
        param = yield response
        if not param:
            return
        response = param + 1
        print(response)


po = plus_one()
po.send(None)
po.send(1)
po.send(55)
po.send(108)

Python协程实现生产者消费者模式

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        r = '200 OK'

def produce(c):
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

c = consumer()
produce(c)

例子来自廖雪峰的 Python 教程,就不多说了,自己到 PyCharm 里调试一下,很快就能明白执行过程。

asyncio 和 async/await

asyncio 是 Python 3.4 引入的标准库, async/await 则是 Python 3.5 引入对使用 asyncio 更好的语法。

简而言之,当我们有多个任务,可以丢到 asyncio 模块的 EventLoop 去,当其中某个任务遇到IO等阻塞操作时,线程不会等待,而是执行 EventLoop 里的下一个任务。

Python 3.4

@asyncio.coroutine
def hello():
    print("Hello world!")
    r = yield from asyncio.sleep(1)
    print("Hello again!")

# 获取EventLoop:
loop = asyncio.get_event_loop()
# 执行coroutine
loop.run_until_complete(hello())
loop.close()

Python 3.5

假设我们有两个 hello() 任务要执行,一个中间会阻塞1秒,另一个中间会阻塞2秒。如果用串行编程,则至少需要3秒。但是如果用协程,在第一个任务阻塞时,第二个任务可以立即开始,这样就节省了1秒。

import asyncio
import time


async def hello(time):
    print(f"Hello world! wait {time} s")
    r = await asyncio.sleep(time)
    print(f"Hello again! come back from {time} s")


async def main():
    print(f"started at {time.strftime('%X')}")
    await asyncio.gather(hello(1), hello(2))
    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

输出(只用了2秒)

started at 21:50:36
Hello world! wait 1 s
Hello world! wait 2 s
Hello again! come back from 1 s
Hello again! come back from 2 s
finished at 21:50:38

Go 中的协程

待补充


Kotlin 中的协程

待补充


参考: