本文深度参考自廖雪峰老师的网站:https://www.liaoxuefeng.com/wiki/1016959663602400/1017968846697824
1 协程
协程(coroutine)的调用类似于子函数,即在函数A的执行过程中去执行子函数B,B执行完成后继续执行A。但使用协程实现的代码没有明显的函数调用。
协程是用同一个线程在不同的函数间切换,随时中断一个函数的执行去执行另一个函数。其执行过程类似于下述方式:
def A():
print('1')
print('2')
print('3')
def B():
print('x')
print('y')
print('z')
执行结果可能为:
1
x
y
2
z
3
协程是由同一个线程交叉去执行多个函数的,相比于多线程,其优点有:
- 协程的执行由代码自身控制,不是线程切换,不存在线程切换的开销,执行效率更高;
- 协程无需多线程的同步控制机制,无需加锁,不存在写变量冲突,同样执行效率更高。
协程只用到了一个线程,对于多核CPU,可以采用多进程+协程的方式,即利用了多核,又利用协程提高了执行效率。
2 通过生成器实现协程
python是通过generator
实现协程的,在generator
中,不但可以通过for
循环进行遍历,还可以通过调用next
函数返回其生成的下一个值。
但yield
不但可以返回下一个值,还可以接收调用者发出的参数。
下面是通过协程实现的生产者-消费者代码:
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[consumer] Consuming %s ...' %n)
r = '200 OK'
def producer(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[producer] Producing %s ...' %n)
r = c.send(n)
print('[producer] Consumer response %s ....' %r)
c.close()
c = consumer()
producer(c)
执行结果:
[producer] Producing 1 ...
[consumer] Consuming 1 ...
[producer] Consumer response 200 OK ....
[producer] Producing 2 ...
[consumer] Consuming 2 ...
[producer] Consumer response 200 OK ....
[producer] Producing 3 ...
[consumer] Consuming 3 ...
[producer] Consumer response 200 OK ....
[producer] Producing 4 ...
[consumer] Consuming 4 ...
[producer] Consumer response 200 OK ....
[producer] Producing 5 ...
[consumer] Consuming 5 ...
[producer] Consumer response 200 OK ....
执行过程为:
- 设置
consumer
为生成器,将其传入producer
; - 在
producer
中调用c.send(None)
,启动生成器; producer
中通过循环生成n
,通过c.send(n)
发送到consumer
,切换到consumer
执行;consumer
通过yield
拿到输入值执行,再通过yield
返回执行结果;producer
执行结束,不再生成新的数据,通过c.close()
关闭consumer
,整个过程结束。
上述整个过程无锁,由同一个线程执行,producer
和consumer
协作完成任务,所以称之为“协程”,而非线程的抢占式多任务。
2 asyncio实现异步IO
python3.4引入了asyncio
,内置了对异步IO的支持。asyncio
的编程模型就是一个消息循环,我们从asyncio
中取得一个EventLoop
的引用,然后把需要执行的协程扔到EventLoop
中执行,就实现了异步IO。
如下面代码所示:
import asyncio
import threading
@asyncio.coroutine
def hello():
print('Hello World! (%s)' %threading.currentThread())
r = yield from asyncio.sleep(1)
print('Hello Again! (%s)' %threading.currentThread())
loop = asyncio.get_event_loop()
tasks = [hello(),hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
执行结果:
Hello World! (<_MainThread(MainThread, started 139742271719168)>)
Hello World! (<_MainThread(MainThread, started 139742271719168)>)
(大约暂停1秒)
Hello Again! (<_MainThread(MainThread, started 139742271719168)>)
Hello Again! (<_MainThread(MainThread, started 139742271719168)>)
代码关键点:
@asyncio.coroutine
装饰符,将一个generator
转换为coroutine
;yield from
获取另一个生成器,以进行指定的异步操作,这里使用asyncio.sleep()
产生一个生成器函数,作用是暂停指定的时间;- 将多个
coroutine
装入列表中,使用asyncio.await
将所有的任务转换为awaitable objects,对所有的任务进行并发执行; asyncio.get_event_loop()
获取一个EventLoop
的引用,使用run_until_complete
等待所有的任务执行完成。- 把
asyncio.sleep(1)
看成是一个耗时1秒的IO操作,在此期间,因为IO操作时不需要CPU,主线程并未等待,而是去执行EventLoop
中其他可以执行的coroutine
了,上面的输出结果也表明了所有的操作都是由同一个线程执行的,因此实现了并发执行。
将上面的sleep
操作换成真正的IO操作,则实现了真正的异步IO,如下面的代码:
import asyncio
@asyncio.coroutine
def wget(host):
print('wget %s...' % host)
connect = asyncio.open_connection(host, 80)
reader, writer = yield from connect
header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
writer.write(header.encode('utf-8'))
yield from writer.drain()
while True:
line = yield from reader.readline()
if line == b'\r\n':
break
print('%s header > %s' % (host, line.decode('utf-8').rstrip()))
# Ignore the body, close the socket
writer.close()
loop = asyncio.get_event_loop()
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
执行结果:
wget www.sohu.com...
wget www.sina.com.cn...
wget www.163.com...
(等待一段时间)
(打印出sohu的header)
www.sohu.com header > HTTP/1.1 200 OK
www.sohu.com header > Content-Type: text/html
...
(打印出sina的header)
www.sina.com.cn header > HTTP/1.1 200 OK
www.sina.com.cn header > Date: Wed, 20 May 2015 04:56:33 GMT
...
(打印出163的header)
www.163.com header > HTTP/1.0 302 Moved Temporarily
www.163.com header > Server: Cdn Cache Server V2.0
...
三个连接由一个线程通过coroutine
进行执行。
总结:asyncio
提供了对异步IO的完整支持;异步操作需要通过在coroutine
中通过yield from
进行实现;多个coroutine
可以组成列表放入EventLoop
中并发执行。
特别注意:基于生成器的协程支持将在python 3.10中移除,官方推荐使用下节将要介绍的async/await
实现协程。
推荐学习:https://blog.csdn.net/SL_World/article/details/86597738
3 async/await
python3.5开始引入新的语法支持异步IO,分别是async
和await
,可以让coroutine
的代码更加简洁易读。
具体替换项为:
- 把
@asyncio.coroutine
替换为async
; - 把
yield from
替换为await
;
代码为:
import asyncio
import threading
async def hello_async():
print('Hello World! (%s)' %threading.currentThread())
r = await asyncio.sleep(1)
print('Hello Again! (%s)' %threading.currentThread())
loop = asyncio.get_event_loop()
tasks = [hello_async(),hello_async()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
输出为:
Hello World! (<_MainThread(MainThread, started 140213464598272)>)
Hello World! (<_MainThread(MainThread, started 140213464598272)>)
暂停约1秒
Hello Again! (<_MainThread(MainThread, started 140213464598272)>)
Hello Again! (<_MainThread(MainThread, started 140213464598272)>)
4 greenlet
greenlets是轻量化的协程,可以单独使用,但通常是和gevent联合使用,提供高层抽象的api以及支持异步IO操作。
4.1 greenlet切换
greenlet类似于线程或Python的内置协程(生成器和async def函数)。通过显式的切换(类似于generator.send(val)
),让不同的greenlet
去执行与其自身对应的函数,switch
时可以传递参数,如下面的代码所示:
>>> from greenlet import greenlet
>>> def test1():
... print("[gr1] main -> test1")
... gr2.switch()
... print("[gr1] test1 <- test2")
... return 'test1 done'
>>> def test2():
... print("[gr2] test1 -> test2")
... gr1.switch()
... print("This is never printed.")
>>> gr1 = greenlet(test1)
>>> gr2 = greenlet(test2)
>>> gr1.switch()
[gr1] main -> test1
[gr2] test1 -> test2
[gr1] test1 <- test2
'test1 done'
>>> gr1.dead
True
>>> gr2.dead
False
4.2 greenlet生命周期
一个greenlet
所指定的函数执行完成后,它的生命周期就结束了,如上面代码所示。可以同时创建多个greenlet
,但同一时刻只有一个greenlet
可以处于运行状态,其余所有的greenlet
都处于等待状态,也就是协程中实际运行的唯一线程一个时刻只能在运行一个函数。可以通过getcurrent()
函数获取当前正在运行的greenlet
对象。
greenlet
可以和python thread
联合使用,一个thead
拥有一个main greenlet
和多个sub greenlets
,隶属于同一个线程的greenlets
之间可以相互switch
,但不能跨线程进行greenlet
的switch
。如下面的代码所示:
from greenlet import getcurrent
from threading import Thread,currentThread
from threading import Event
started = Event()
switched = Event()
class T(Thread):
def run(self):
print('sub thread : %s' % currentThread())
print('sub thread\'s greenlet %s' %getcurrent())
self.glet = getcurrent()
started.set()
switched.wait()
print('main thread : %s' % currentThread())
print('main thread\'s greenlet %s' %getcurrent())
t = T()
t.start()
_ = started.wait()
t.glet.switch()
switched.set()
t.join()
输出:
main thread : <_MainThread(MainThread, started 140349767833344)>
main thread's greenlet <greenlet.greenlet object at 0x7fa5b736b930>
sub thread : <T(Thread-7, started 140349624043264)>
sub thread's greenlet <greenlet.greenlet object at 0x7fa5b736ba60>
报错:
发生异常: error
cannot switch to a different thread
File "/***.py", line 39, in <module>
t.glet.switch()
但要注意,线程的main greenlet
永远不会结束生命周期,因此,如果将一个线程的main greenlet
的引用传递给了另一个线程,要仔细审核其生命周期,避免使用线程已结束的main greenlet
。
4.3 greenlet’s parents
main greenlet
的parent是None。其余的greenlet
都是有其parent的,谁创建了当前的greenlet
,谁就是当前greenlet
的parent。当前greenlet
生命周期结束后,就回到其parent处。4.1节的示例代码中,gr1
和gr2
的parent都是main greenlet
,某一个结束后都是回到main greenlet
。
关于greenlet的理解还相当粗浅,后续使用到之后补充。
参考资料:
从yield from到async的使用
从0到1,Python异步编程的演进之路
Python Async/Await入门指南