浅析协程
前言
以前我们的程序都是单线程,只有一个控制流,在像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
执行过程如下:
a = g.send(None)
,进入funny()
- 协程执行 yield 5,返回5
- 主程序
print(a)
,打印了5 - 主程序
g.send(666)
- 协程从 yield 5 处继续执行,注意,yield 5 之后,不是
print(param)
,而是赋值语句param =
print(param)
,把 666 打印出来。yield 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 中的协程
待补充
参考:
- 《深入理解Kotlin协程》
- 廖雪峰的 Python 教程