协程
-
协程是什么?
协程的特点在于在一个线程中执行。
-
和多线程相比
和多线程相比,协程有如下优势
- 协程极高的执行效率。协程的切换不会造成线程切换,没有线程切换的开销,和多线程相比,协程数量越多,协程的性能优势就越明显。
- 协程不需要多线程的锁机制。因为只有一个线程,不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高。
Python对协程的支持是通过generator实现的。 在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。什么时候会死锁?
通过协程实现生产者消费者。生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率很高。
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % (n,))
r = '200K'
def produce(c):
c.send(None) # 调用c.send(None)启动生成器
n = 0
while n < 5:
n += 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
if __name__ == '__main__':
c = consumer()
produce(c)
注意到consumer函数是一个generator,把一个consumer传入produce后:
- 首先调用c.send(None)启动生成器; c.send(None)第一次进入生成器,运行到yield为止,下一次从给n赋值开始继续运行。
- 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
- consumer通过yield拿到消息,处理,又通过yield把结果传回; 首先将c.send(n)中的参数n赋值给consumer中的yield语句前的n,然后继续往下执行判断和输出语句,直到直接return或者到达下一个yield语句。
- produce拿到consumer处理的结果,继续生产下一条消息;
- produce决定不生产了,通过c.close()关闭consumer,整个过程结束。
整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
asyncio
几个基本概念
event_loop事件循环:程序开启一个无限的循环,程序员会把一些函数(协程)注册到事件循环上。当满足事件发生的条件时,调用相应的协程函数。
coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。
future 对象: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别。
task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。Task 对象是 Future 的子类,它将 coroutine 和 Future 联系在一起,将 coroutine 封装成一个 Future 对象。
async/await 关键字:python3.5 用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口。其作用在一定程度上类似于yield。
实践
asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。
asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。
import asyncio
@asyncio.coroutine
def hello():
print("Hello world!")
# 异步调用asyncio.sleep(1):
r = yield from asyncio.sleep(1)
print("Hello again!")
# 获取EventLoop:
loop = asyncio.get_event_loop()
# 执行coroutine
loop.run_until_complete(hello())
loop.close()
@asyncio.coroutine把一个generator标记为coroutine类型,然后把这个coroutine扔到EventLoop中执行。
hello()会首先打印出hello world!,然后yield from语法可以让我们方便地调用另一个generator。由于asyncio.sleep()也是一个coroutine,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。
asyncio.sleep(1)看成是一个耗时1秒的IO操作,在此期间,主线程并未等待,而是去执行EventLoop中其他可以执行的coroutine了,因此可以实现并发执行。
asyncio可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力并不大。如果把asyncio用在服务器端,例如web服务器,由于HTTP连接就是IO操作,因此可以用单线程+coroutine实现多用户的高并发支持。asyncio实现了TCP、UDP、SSL等协议,aiohttp则是基于asyncio实现的HTTP框架。
async
用asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。
为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。
请注意,async和await是针对coroutine的新语法,要使用新的语法,只需要做两步简单的替换:
- 把
@asyncio.coroutine替换为async; - 把
yield from替换为await。
异步函数(协程)
参考:zhuanlan.zhihu.com/p/27258289
使用asyc修饰将普通函数和生成器函数包装成异步函数和异步生成器。
普通函数和生成器函数
def function():
return 1
def generator():
yield 1
使用async修饰将普通函数和生成器函数包装成异步函数和异步生成器。 异步函数(协程)
async def async_function():
return 1
异步生成器
async def async_generator():
yield 1
print("type(func):", type(func) is types.FunctionType) # True
print("type(generator):", type(generator()) is types.GeneratorType) # True
# 直接调用异步函数不会返回结果,而是返回一个coroutine对象
print("type(async_func):", type(async_func()) is types.CoroutineType) # True
print("type(async_generator):", type(async_generator()) is types.AsyncGeneratorType) # True
直接调用异步函数不会返回结果,而是返回一个coroutine对象。 协程需要通过其他方式来驱动,因此可以使用这个协程对象的send方法给协程对象发送一个值。
async_func().send(None)
但是上面的调用会抛出一个异常。
async_func().send(None)
StopIteration: 1
这是因为生成器/协程在正常返回退出时会抛出一个StopIteration异常,而原来的返回值会存放在StopIteration对象的value属性中,通过对异常捕获可以获取协程真正的返回值。
try:
async_func().send(None)
except StopIteration as e:
print(e.value)
在协程函数中,可以通过await语法来挂起自身协程,并等待另一个协程直到返回结果。 await语法只能出现在通过async修饰的函数中,否则会报SyntaxError错误。而且await后面的对象需要一个Awaitable,或者实现了相关的协议。
异步生成器
完成异步的代码不一定要用async/await,使用了async/await的代码也不一定能做到异步,async/await是协程的语法糖,使协程之间的调用变得更加清晰,使用async修饰的函数调用时会返回一个协程对象,await只能放在async修饰的函数里面使用,await后面必须要跟着一个协程对象或Awaitable,await的目的是等待协程控制流的返回,而实现暂停并挂起函数的操作是yield。
实例
asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步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()
if __name__ == '__main__':
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()
@asyncio.coroutine把一个generator标记为coroutine类型,然后,我们把这个coroutine放到事件循环中执行。
yield from语法可以让我们更方便地调用另一个generator。所以线程不会等待IO操作,而是直接中断并执行下一个消息循环。当yield from返回时,线程就可以从yield from拿到返回值,然后接着执行下一行语句。
在此期间,主线程并未等待,而是执行事件循环中其他可以执行的coroutine了,因此,用Task封装的三个coroutine可实现由同一个线程并发执行。
用async和await语法实现的在单线程中并发执行三个coroutine:
import asyncio
async def wget(host):
print('wget %s...' % host)
connect = asyncio.open_connection(host, 80)
reader, writer = await connect
header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
writer.write(header.encode('utf-8'))
await writer.drain()
while True:
line = await 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()
if __name__ == '__main__':
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()
参考文献
- 协程和async关键字:zhuanlan.zhihu.com/p/27258289
- 异步IO:www.liaoxuefeng.com/wiki/101695…
- yield关键字:blog.csdn.net/mieleizhi05…
- asyncio:www.liaoxuefeng.com/wiki/101695…
- async:zhuanlan.zhihu.com/p/27258289
- python的异步IO框架aysncio:juejin.cn/post/684490…
- 协程实现生产者消费者:www.liaoxuefeng.com/wiki/101695…