背景
协程的概念最早提出于1963年,先于线程产生,但不符合当时自顶向下的程序设计思想,不流行。
20世纪60年代,进程的概念被引入,进程作为操作系统资源分配和调度的基本单位,多进程的方式在很长时间内大大提高了系统运行的效率,虽然中间产生了Copy-On-Write等技术的出现,但进程的频繁创建和销毁代价较大、资源的大量复制和分配耗时仍然较高,于是80年代出现了能够独立运行的单位--线程。
多线程之间可以直接共享资源,同时线程之间的通信效率又远高于进程间,将任务并发的性能再次向前推进了一大步。
不过多线程也有其不足的地方,虽然线程之间切换代价相较多进程小了很多,但一些场景下线程CPU时间片的大量切换其实是做了很多不必要的无用功,特别是Python中因为GIL锁的存在,其多线程很多时候并不能提高程序运行效率。
于是协程的概念又开始发挥了作用,只有一个线程在执行,只有当该子程序内部发生中断或阻塞时,才会交出线程的执行权交给其他子程序,在适当的时候再返回来接着执行。这省去了线程间频繁切换的时间开销同时也解决了多线程加锁造成的相关问题。
通常在Python中我们进行并发编程一般都是使用多线程或者多进程来实现的,对于计算型任务由于GIL的存在我们通常使用多进程来实现,而对于IO型任务我们可以通过线程调度来让线程在执行IO任务时让出GIL,从而实现表面上的并发。
其实对于IO型任务我们还有一种选择就是协程,协程是运行在单线程当中的"并发",协程相比多线程一大优势就是省去了多线程之间的切换开销,获得了更大的运行效率。
是什么?
协程,又称微线程,纤程,英文名Coroutine。
协程的作用是在执行函数A遇到IO阻塞可以去执行函数B,然后函数B遇到IO阻塞继续执行函数A(可以自由切换)。但这一过程并不是函数调用,这一整个过程看似像多线程,然而协程只有一个线程执行。
优势
执行效率极高,因为子程序切换(函数)不是线程切换,由程序自身控制,没有切换线程的开销。所以与多线程相比,线程的数量越多,协程性能的优势越明显。
不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁,因此执行效率高很多。
协程可以处理IO密集型程序的效率问题,但是处理CPU密集型不是它的长处,如要充分发挥CPU利用率可以结合多进程+协程。
适用场景
多进程+协程
具体的生产环境中,Python项目经常会使用多进程+协程的方式,规避GIL锁的问题,充分利用多核的同时又充分发挥协程高效的特性。
运作机制
协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
默认多线程是内核控制的,而协程的调度完全由用户控制。
发展
Python中的协程经历了很长的一段发展历程。
其大概经历了如下三个阶段:
最初的生成器变形yield/send
引入@asyncio.coroutine和yield from *
引入async/await关键字
Python2.x对协程的支持比较有限,生成器yield实现了一部分但不完全,gevent模块倒是有比较好的实现。
Python3.4加入了asyncio模块。
Python3.5中又提供了async/await语法层面的支持。
Python3.6中asyncio模块更加完善和稳定。
接下来我们围绕这些内容详细阐述一下。
Python2.x协程
python2.x实现协程的方式有:
yield + send
gevent (见后续章节)
yield + send(利用生成器实现协程)
我们通过“生产者-消费者”模型来看一下协程的应用,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产。
#-*- coding:utf8 -*-
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 return: %s' % r)
c.close()
if __name__ == '__main__':
c = consumer()
producer(c)
send(msg)与next()的区别在于send可以传递参数给yield表达式,这时传递的参数会作为yield表达式的值,而yield的参数是返回给调用者的值。换句话说,就是send可以强行修改上一个yield表达式的值。比如函数中有一个yield赋值a = yield 5,第一次迭代到这里会返回5,a还没有赋值。第二次迭代时,使用send(10),那么就是强行修改yield 5表达式的值为10,本来是5的,结果a = 10。send(msg)与next()都有返回值,它们的返回值是当前迭代遇到yield时,yield后面表达式的值,其实就是当前迭代中yield后面的参数。第一次调用send时必须是send(None),否则会报错,之所以为None是因为这时候还没有一个yield表达式可以用来赋值。上述例子运行之后输出结果如下:
[PRODUCER]Producing 1...
[CONSUMER]Consuming 1...
[PRODUCER]Consumer return: 200 OK
[PRODUCER]Producing 2...
[CONSUMER]Consuming 2...
[PRODUCER]Consumer return: 200 OK
[PRODUCER]Producing 3...
[CONSUMER]Consuming 3...
[PRODUCER]Consumer return: 200 OK
[PRODUCER]Producing 4...
[CONSUMER]Consuming 4...
[PRODUCER]Consumer return: 200 OK
[PRODUCER]Producing 5...
[CONSUMER]Consuming 5...
[PRODUCER]Consumer return: 200 OK
Python3.x协程
除了Python2.x中协程的实现方式,Python3.x还提供了如下方式实现协程:
asyncio + yield from (python3.4+)
asyncio + async/await (python3.5+)
Python3.4以后引入了asyncio模块,可以很好的支持协程。
Python3.4 asyncio + yield from
asyncio是Python3.4版本引入的标准库,直接内置了对异步IO的支持。asyncio的异步操作,需要在coroutine中通过yield from完成。
看如下代码(需要在Python3.4以后版本使用):
#-*- coding:utf8 -*-
import asyncio
@asyncio.coroutine
def test(i):
print('test_1', i)
r = yield from asyncio.sleep(1)
print('test_2', i)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [test(i) for i in range(3)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
@asyncio.coroutine把一个generator标记为coroutine类型,然后就把这个coroutine扔到EventLoop中执行。test()会首先打印出test_1,然后yield from语法可以让我们方便地调用另一个generator。由于asyncio.sleep()也是一个coroutine,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。把asyncio.sleep(1)看成是一个耗时1秒的IO操作,在此期间主线程并未等待,而是去执行EventLoop中其他可以执行的coroutine了,因此可以实现并发执行。
Python3.5 asyncio + async/await
为了简化并更好地标识异步IO,从Python3.5开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。
请注意,async和await是coroutine的新语法,使用新语法只需要做两步简单的替换:
把@asyncio.coroutine替换为async
把yield from替换为await
看如下代码(在Python3.5以上版本使用):
#-*- coding:utf8 -*-
import asyncio
async def test(i):
print('test_1', i)
await asyncio.sleep(1)
print('test_2', i)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [test(i) for i in range(3)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
Python3.7中 asyncio引入了新的语法
asyncio.run() # 将协程函数放到异步环境里执行
asyncio.create_task()
asyncio.gather()
#-*- coding:utf8 -*-
import asyncio
async def foo():
print('start foo')
await asyncio.sleep(1)
print('----end foo')
if __name__ == '__main__':
asyncio.run(foo())
如何将多个任务(协程)同时执行?
1.采用函数gather
async def foo():
print('----start foo')
await asyncio.sleep(1)
print('----end foo')
async def bar():
print('****start bar')
await asyncio.sleep(2)
print('****end bar')
async def main():
res = await asyncio.gather(foo(), bar())
print(res)
if __name__ == '__main__':
asyncio.run(main())
返回值为函数的返回值列表 本例中为[None, None]
官方文档中的解释是
await asyncio.gather(*aws, loop=None, return_exceptions=False)
并发 运行 aws 序列中的可等待对象。
如果 aws 中的某个可等待对象为协程,它将自动作为一个任务加入日程。
如果所有可等待对象都成功完成,结果将是一个由所有返回值聚合而成的列表。结果值的顺序与 aws 中可等待对象的顺序一致。
如果 return_exceptions 为 False (默认),所引发的首个异常会立即传播给等待 gather() 的任务。aws 序列中的其他可等待对象 不会被取消 并将继续运行。
如果 return_exceptions 为 True,异常会和成功的结果一样处理,并聚合至结果列表。
如果 gather() 被取消,所有被提交 (尚未完成) 的可等待对象也会 被取消。
如果 aws 序列中的任一 Task 或 Future 对象 被取消,它将被当作引发了 CancelledError 一样处理 – 在此情况下gather() 调用不会被取消。这是为了防止一个已提交的 Task/Future 被取消导致其他 Tasks/Future 也被取消。
2.创建task
asyncio.create_task(coro)
将 coro 协程打包为一个Task排入日程准备执行。返回 Task 对象。
该任务会在 get_running_loop() 返回的loop中执行,如果当前线程没有在运行的loop则会引发 RuntimeError。
此函数在Python 3.7中被加入。
在Python 3.7之前,可以改用低层级的 asyncio.ensure_future() 函数。
async def foo():
print('----start foo')
await asyncio.sleep(1)
print('----end foo')
async def bar():
print('****start bar')
await asyncio.sleep(2)
print('****end bar')
async def main():
asyncio.create_task(foo())
asyncio.create_task(bar())
if __name__ == '__main__':
asyncio.run(main())
但是运行一下就会发现, 只输出了
----start foo
****start bar
这是因为create_task函数只是把任务打包放进了队列,至于它们有没有运行完,不管。
因此需要等待它们执行完毕.
最后的代码为
async def foo():
print('----start foo')
await asyncio.sleep(1)
print('----end foo')
async def bar():
print('****start bar')
await asyncio.sleep(2)
print('****end bar')
async def main():
task1 = asyncio.create_task(foo())
task2 = asyncio.create_task(bar())
await task1
await task2
if __name__ == '__main__':
asyncio.run(main())
如果有多个请求:
async def foo():
print('----start foo')
await asyncio.sleep(1)
print('----end foo')
async def main():
tasks = []
for i in range(10):
tasks.append(asyncio.create_task(foo()))
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
async def foo():
print('----start foo')
await asyncio.sleep(1)
print('----end foo')
async def bar():
print('****start bar')
await asyncio.sleep(2)
print('****end bar')
async def main():
tasks = []
for i in range(10):
tasks.append(asyncio.create_task(foo()))
for j in range(10):
tasks.append(asyncio.create_task(bar()))
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
异步嵌套:
async def foo():
print('----start foo')
await asyncio.sleep(1)
print('----end foo')
async def bar():
print('****start bar')
await asyncio.sleep(2)
print('****end bar')
async def foos():
print('----------------------')
tasks = []
for i in range(3):
tasks.append(asyncio.create_task(foo()))
await asyncio.wait(tasks)
async def main():
tasks = []
for i in range(3):
tasks.append(asyncio.create_task(foos()))
for j in range(3):
tasks.append(asyncio.create_task(bar()))
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
Gevent
Gevent是一个基于Greenlet实现的网络库,通过greenlet实现协程。基本思想是一个greenlet就认为是一个协程,当一个greenlet遇到IO操作的时候,比如访问网络,就会自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO操作。
Greenlet是作为一个C扩展模块,它封装了libevent事件循环的API,可以让开发者在不改变编程习惯的同时,用同步的方式写异步IO的代码。
#-*- coding:utf8 -*-
import gevent
def test(n):
for i in range(n):
print(gevent.getcurrent(), i)
if __name__ == '__main__':
g1 = gevent.spawn(test, 3)
g2 = gevent.spawn(test, 3)
g3 = gevent.spawn(test, 3)
g1.join()
g2.join()
g3.join()
# 运行结果:
<Greenlet at 0x10a6eea60: test(3)> 0
<Greenlet at 0x10a6eea60: test(3)> 1
<Greenlet at 0x10a6eea60: test(3)> 2
<Greenlet at 0x10a6eed58: test(3)> 0
<Greenlet at 0x10a6eed58: test(3)> 1
<Greenlet at 0x10a6eed58: test(3)> 2
<Greenlet at 0x10a6eedf0: test(3)> 0
<Greenlet at 0x10a6eedf0: test(3)> 1
<Greenlet at 0x10a6eedf0: test(3)> 2
可以看到3个greenlet是依次运行而不是交替运行。要让greenlet交替运行,可以通过gevent.sleep()模拟IO阻塞,就会交出控制权:
def test(n):
for i in range(n):
print(gevent.getcurrent(), i)
gevent.sleep(1)
# 运行结果:
<Greenlet at 0x10382da60: test(3)> 0
<Greenlet at 0x10382dd58: test(3)> 0
<Greenlet at 0x10382ddf0: test(3)> 0
<Greenlet at 0x10382da60: test(3)> 1
<Greenlet at 0x10382dd58: test(3)> 1
<Greenlet at 0x10382ddf0: test(3)> 1
<Greenlet at 0x10382da60: test(3)> 2
<Greenlet at 0x10382dd58: test(3)> 2
<Greenlet at 0x10382ddf0: test(3)> 2
当然在实际的代码里,我们不会用gevent.sleep()去切换协程,而是在执行到IO操作时gevent会自动完成,所以gevent需要将Python自带的一些标准库的运行方式由阻塞式调用变为协作式运行。这一过程在启动时通过monkey patch完成:
#-*- coding:utf8 -*-
from gevent import monkey
monkey.patch_all()
from urllib import request
import gevent
def test(url):
print('Get: %s' % url)
response = request.urlopen(url)
content = response.read().decode('utf8')
print('%d bytes received from %s.' % (len(content), url))
if __name__ == '__main__':
gevent.joinall([
gevent.spawn(test, 'http://httpbin.org/ip'),
gevent.spawn(test, 'http://httpbin.org/uuid'),
gevent.spawn(test, 'http://httpbin.org/user-agent')
])
# 运行结果:
Get: http://httpbin.org/ip
Get: http://httpbin.org/uuid
Get: http://httpbin.org/user-agent
53 bytes received from http://httpbin.org/uuid.
40 bytes received from http://httpbin.org/user-agent.
31 bytes received from http://httpbin.org/ip.
从结果看,3个网络操作是并发执行的,而且结束顺序不同,但只有一个线程。