协程很久之前就知道这东西,但是 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: n = yield r if not n: return print('[CONSUMER] Consuming %s...' % n) r = 'success'
def produce(c): 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())) 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.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): 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 项目,目前还在开发阶段。