python异步IO

591 阅读8分钟

协程

  1. 协程是什么?

    协程的特点在于在一个线程中执行。

  2. 和多线程相比

    和多线程相比,协程有如下优势

    • 协程极高的执行效率。协程的切换不会造成线程切换,没有线程切换的开销,和多线程相比,协程数量越多,协程的性能优势就越明显。
    • 协程不需要多线程的锁机制。因为只有一个线程,不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高。

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后:

  1. 首先调用c.send(None)启动生成器; c.send(None)第一次进入生成器,运行到yield为止,下一次从给n赋值开始继续运行。
  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
  3. consumer通过yield拿到消息,处理,又通过yield把结果传回; 首先将c.send(n)中的参数n赋值给consumer中的yield语句前的n,然后继续往下执行判断和输出语句,直到直接return或者到达下一个yield语句。
  4. produce拿到consumer处理的结果,继续生产下一条消息;
  5. 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开始引入了新的语法asyncawait,可以让coroutine的代码更简洁易读。 请注意,asyncawait是针对coroutine的新语法,要使用新的语法,只需要做两步简单的替换:

  1. @asyncio.coroutine替换为async
  2. 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可实现由同一个线程并发执行

asyncawait语法实现的在单线程中并发执行三个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()

参考文献

  1. 协程和async关键字:zhuanlan.zhihu.com/p/27258289
  2. 异步IO:www.liaoxuefeng.com/wiki/101695…
  3. yield关键字:blog.csdn.net/mieleizhi05…
  4. asyncio:www.liaoxuefeng.com/wiki/101695…
  5. async:zhuanlan.zhihu.com/p/27258289
  6. python的异步IO框架aysncio:juejin.cn/post/684490…
  7. 协程实现生产者消费者:www.liaoxuefeng.com/wiki/101695…