协程很久之前就知道这东西,但是 Java 没有,也就没怎么去了解,最近在学 Python 看到协程,做个记录。

概念

说到协程一般都会联系到进程和线程,通常请款下这三者的比较如下:

  • 进程:程序执行的一个实例,一个进程最少包含一个线程,不同进程之间的切换代价大;
  • 线程:CPU 调度的基本单位,进程的一个实体,线程的上下文切换代价比进程小;
  • 协程:是一种用户态的轻量级线程,一个线程可包含多个协程。

协程的最大的优势是极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

Python 中的协程

生成器 generator 和 yield 关键字

如果一个函数定义中包含 yield 关键字,那这个函数就是一个 generator 函数。

yield 的语法规则是:在yield这里暂停函数的执行,并返回yield后面表达式的值(默认为 None),直到被 next() 方法再次调用时,从上次暂停的 yield 代码处继续往下执行。当没有可以继续 next() 的时候,抛出异常,该异常可被 for 循环处理。

每个生成器都可以执行 send() 方法,为生成器内部的 yield 语句发送数据。Python 对协程的支持是通过 generator 实现的。

看一个生产者和消费者的例子,生产者生产消息后 yield 跳转消费者消费,消费者执行后又跳回生产者继续生产,任务都在一个线程内部完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def consumer():
r = 'start task...'
while True:
# 遇到 yield 语句返回,再次执行从上次返回的 yield 语句处继续执行
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = 'success'

def produce(c):
# 第一次运行生成器用 send() 函数,传入 None 参数启动生成器
print(c.send(None))
n = 0
while n < 3:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()

c = consumer()
produce(c)

输出

1
2
3
4
5
6
7
8
9
10
start task...
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: success
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: success
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: success

@asyncio.coroutine 和 yield from

@asyncio.coroutine 标记一个生成器为协程,yield from 即等待另一个协程的返回。asyncio 是 Python3.4 开始引入的一个基于时间循环的异步 IO 模块

asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个 EventLoop 的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步 IO。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import asyncio, datetime

@asyncio.coroutine
def task(name):
print("Start: {} Time: {}".format(name, datetime.datetime.now()))
# 延时模拟 IO 任务,比如网络请求或者写文件等等
yield from asyncio.sleep(2)
print('continue other task: ', name)
print("End: {} Time: {}".format(name, datetime.datetime.now()))

loop = asyncio.get_event_loop()
tasks = [task('t1'), task('t2')]
#loop.run_until_complete(asyncio.wait(tasks))
# wait 和 gather 返回值有所不同
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()

输出

1
2
3
4
5
6
7
8
/coroutin/test.py:6: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
def task(name):
Start: t1 Time: 2022-02-26 00:43:01.195497
Start: t2 Time: 2022-02-26 00:43:01.196419
continue other task: t1
End: t1 Time: 2022-02-26 00:43:03.199804
continue other task: t2
End: t2 Time: 2022-02-26 00:43:03.200136

这里环境用的是 Python3.8,可以看到警告,新版本不推荐用 @coroutine,可以用 async def 来定义协程。

async 和 await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import asyncio, datetime

async def task(name):
print("Start: {} Time: {}".format(name, datetime.datetime.now()))
await asyncio.wait([ioTask(name)])
print('continue other task: ', name)
print("End: {} Time: {}".format(name, datetime.datetime.now()))

async def ioTask(name):
# 延时模拟 IO 任务,比如网络请求或者写文件等等
print('execute io task: ', name)
await asyncio.sleep(2)

loop = asyncio.get_event_loop()
tasks = [task('t1'), task('t2')]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

输出

1
2
3
4
5
6
7
8
Start: t2 Time: 2022-02-26 00:46:01.575044
Start: t1 Time: 2022-02-26 00:46:01.575539
execute io task: t2
execute io task: t1
continue other task: t2
End: t2 Time: 2022-02-26 00:46:03.580642
continue other task: t1
End: t1 Time: 2022-02-26 00:46:03.580710

协程适合 IO 密集型,不适合 CPU 密集型应用。

Java 为什么没有协程

在当前已发行版 Java 中,还没有协程,通常使用协程的意义是为了节省创建和切换线程带来的开销,但是在 Java 一直都有其他的解决方式,比如:

  • 有 Netty 这类非阻塞的I/O客户端-服务器框架;
  • 线程池解决了线程创建和销毁的开销;
  • JDK 也有 JUC 等完备的工具用于异步编程。

其他语言使用协程最大的好处是写法简单优雅,写起来是同步的,跑起来是异步的。相比之下 Java 的异步线程写法就复杂得多,而且 Java 一直以来都被吐槽太过繁琐。

目前 Java 也有在推动协程库的开发,这个就是 Loom 项目,目前还在开发阶段。