50

asyncio:高性能异步模块使用介绍

 5 years ago
source link: http://www.10tiao.com/html/761/201807/2650368555/1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文是作者「无名小妖」的第3篇原创投稿,文章重点探讨 Python3 中的 asyncio 库,这是官方推荐实现高并发的模块。如果你喜欢技术写作并且愿意分享,可投稿给我(公众号菜单有联系方式)。文章采纳后会有 50~100元 稿费。

本文Asyncio 从 3.4 开始成为 Python 生态系统的一部分,从那时起因其令人印象深刻的速度和易用性,它成为了大量 Python 库和框架的基础。

Asyncio 允许您轻松地编写利用协程的单线程并发程序,这些协程就像一个个被剥离的线程,并不会出现像使用线程时会遇到问题(要记住保留锁保护程序中的重要部分,要防止多步操作在执行的过程中中断,要防止数据处于无效状态)。

Asyncio还做了很好的工作,将我们从I/O多路复用访问的复杂性抽象出来,它还是程序线程安全的,因为在任意时刻只有一个协程运行。

入门

为了使用Asyncio,我们需要了解一下事件循环和协程。所有基于异步的系统都需要一个事件循环,事件循环调度我们的异步子程序并分配不同任务的执行。协程本质上是传统的线程的轻量级版本,它与线程非常相似,通过使用协程,我们基本上可以自己编写异步程序。

对于不太了解同步异步的同学在这里略作解释(已经知道的可以直接看示例):

同步阻塞,就好比火车站过安检,需要你耗费几分钟时间,都检查完了再进站,每个人都要耽误几分钟。

同步非阻塞,我们假设火车站提供了一种服务名叫“反馈”,你交10块钱就可以加一个微信号,然后你把车票、身份证、行李一次性放到一个地方,同时人家还保存了一下你的美照(这一系列操作后面统称“打包”),这样你可以直接进站买点东西呀上个厕所呀(后面统称“闲逛”),再通过微信不断询问 我的票检查好了吗? 查好了吗? 直到那头回复你说“好了”,你到指定地点去把你刚才打的包取回(后面统称“取包”),结束。

异步阻塞,你交20块钱买了“反馈2.0”—检查完毕人家会主动发微信告诉你,不需要你在不断询问了,而你“打包”完毕,还在检票口傻等,直到人家说“好了”,你在“取包”。这其实没有任何意义,因为你还是在等待,真正有意义的是异步非阻塞。

异步非阻塞,你交20块钱买了“反馈2.0”,“打包”完毕,“闲逛”,直到人家说“好了”,然后你“取包”。这才是真正把你解放了,既不用等着,也不用不断询问。而本文的asyncio用的就是异步非阻塞的协程。

我们可以定义一个事件循环,用于执行一个简单的协程。

示例 1

import asyncio

async def MyCoroutine():  # 一个简单的协程
   print("Hello, world!")

def main():
   # 事件循环
   loop = asyncio.get_event_loop()
   # 运行事件循环,直到分配的所有任务已经完成
   loop.run_until_complete(MyCoroutine())
   loop.close()

if __name__ == '__main__':
   main()

示例1 能够成功运行,可是它并没有带来什么好处,因为本例的目的在于让大家明白事件循环和协程的使用方式。在更复杂的场景中,我们才真正看到它在性能上的好处。

示例2

import asyncio

async def MyCoroutine(future):
   # 使用asyncio.sleep模拟一些耗时的操作(一般是一些IO操作,例如网络请求,文件读取(就是“过安检”这个动作))
   await asyncio.sleep(1)
   # 设定future对象的返回结果
   future.set_result("myfuture 已执行")

async def main():
   # 定义一个future对象,asyncio.Future的实例表示将来要完成的任务
   future = asyncio.Future()
   # ensure_future方法 接收协程或者future作为参数,作用是排定他们的执行时间。
   await asyncio.ensure_future(MyCoroutine(future))
   # future.result()返回可调用对象的结果,或者重新抛出执行可调用的对象时抛出的异常。
   print(future.result())

# 将main加入事件循环
loop = asyncio.get_event_loop()
try:
   loop.run_until_complete(main())
finally:
   loop.close()

示例2 对一些方法进行了介绍,为我们使用asyncio提升性能做一些铺垫。这里涉及到了一个future的概念:future对象表示将来发生的事。可以对比期权、期房来理解。

现在让我们尝试用asyncio同时运行多个协程。这将让你体会asyncio的强大,以及如何使用它来有效地创建一个在单个线程上运行的性能难以置信的Python程序。

示例3

import asyncio
import random

async def MyCoroutine(id):
   process_time = random.randint(1, 5)
   await asyncio.sleep(process_time)
   print("协程: {}, 执行完毕。用时: {} 秒".format(id, process_time))

async def main():
   tasks = [asyncio.ensure_future(MyCoroutine(i)) for i in range(10)]
   await asyncio.gather(*tasks)

loop = asyncio.get_event_loop()
try:
   loop.run_until_complete(main())
finally:
   loop.close()

为了便于阅读,示例3没有进行注释。首先创建一个协程,它以ID为参数,生成一个名为process_time的1-5的随机整数,并等待该时间长度,最后它会打印出它的ID以及它等待了多长时间。然后我们生成了10个不同的任务,最后在事件循环里运行这些任务。
输出:

协程: 2, 执行完毕。用时: 1
协程: 5, 执行完毕。用时: 1
协程: 3, 执行完毕。用时: 1
协程: 9, 执行完毕。用时: 2
协程: 1, 执行完毕。用时: 3
协程: 8, 执行完毕。用时: 3
协程: 7, 执行完毕。用时: 3
协程: 0, 执行完毕。用时: 4
协程: 6, 执行完毕。用时: 4
协程: 4, 执行完毕。用时: 5

从输出结果可以看出两点:1.协程并没有按照顺序返回结果;2.批量运行的任务所用的时间和所有任务中用时最长的相同。这就好比做饭的时候,先蒸米饭用时15分钟,在蒸米饭期间又炒了两个菜(当然不是一个人在炒),一个用了8分钟一个用了12分钟,所以先上的是用了8分钟的菜,然后是12分钟的,最后才是米饭。并且最后总用时是15分钟,而不是35分钟。这就是异步带来的效率提升!如果你觉得提升并不明显,不妨把任务数提升到100或者1000……

另外,示例中我们使用的是ensure_future和gather,相对应的还有create_task和wait,也能起到类似的作用。

ensure_future接收的参数是协程或者future对象,create_task接收的参数只能是协程。

asyncio.gather 接收的参数是协程或者future对象,返回所有传入协程或者future的结果集。asyncio.wait 只接收future对象,返回两组future对象(已完成和等待),它有个timeout参数可用于控制返回之前等待的最大秒数,还有个return_when参数可以控制在什么情况下让函数返回。

详细的可以看官网链接:

  • ensure_future:https://docs.python.org/3/library/asyncio-task.html#asyncio.ensure_future

  • create_task:https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.AbstractEventLoop.create_task

  • gather:https://docs.python.org/3/library/asyncio-task.html#asyncio.gather

  • wait:https://docs.python.org/3/library/asyncio-task.html#asyncio.wait

应用

现在的asyncio,已经有了很多的模块在支持:aiohttp,aiopg,aioredis等等, 可以在这里查看: https://github.com/aio-libs 。下面我们来了解其中一个模块aiofiles的使用。

示例4

import asyncio
import aiofiles

async def myopen():
   async with aiofiles.open('333.log', encoding='utf8') as file:
       contents = await file.read()
   print('my read done, file size is {}'.format(len(contents)))

async def test_read():
   print('begin readfile')
   await myopen()
   print('end readfile')

async def test_cumpute(x, y):
   print("begin cumpute")
   await asyncio.sleep(0.2)
   print('end cumpute')
   return x + y

loop = asyncio.get_event_loop()
to_do = [asyncio.ensure_future(test_read()), asyncio.ensure_future(test_cumpute(1, 5))]
loop.run_until_complete(asyncio.wait(to_do))
loop.close()

仔细观察示例4的代码我们应该能够猜出输出的顺序是什么:由于事件循环里test_read先加入,test_cumpute后加入,所以代码一运行必然先输出begin readfile和begin cumpute,然后看这两个谁的执行时间更短,如果test_cumpute执行时间更短那么就先输出end cumpute,反之则最后输出end cumpute。由于本例的目的是要模拟在进行一个长时间阻塞任务的同时,做一些其他事,所以文件要稍微大一些,在读取文件期间做一个计算。笔者测试用的文件‘333.log’有160M,输出如下:

begin readfile
begin cumpute
end cumpute
my read done, file size is 162538819
end readfile

由此可见,在读取文件的过程中计算已经完成。美中不足的是我们并没有获取到test_cumpute计算的结果,如何获取计算结果呢? 请看代码:
示例5

import asyncio
import aiofiles

async def myopen():
   async with aiofiles.open('333.log', encoding='utf8') as file:
       contents = await file.read()
   print('my read done, file size is {}'.format(len(contents)))

async def test_read():
   print('begin readfile')
   await myopen()
   print('end readfile')

async def test_cumpute(x, y):
   print("begin cumpute")
   await asyncio.sleep(0.2)
   print('end cumpute')
   return x + y

def got_result(future):
   print('The result is ', future.result())

loop = asyncio.get_event_loop()
to_do = [asyncio.ensure_future(test_read()), asyncio.ensure_future(test_cumpute(1, 5))]
to_do[1].add_done_callback(got_result)
loop.run_until_complete(asyncio.wait(to_do))
loop.close()

示例5比示例4只多了如下两句:

def got_result(future):
   print('The result is ', future.result())

to_do[1].add_done_callback(got_result)

示例5引出了add_done_callback,通过add_done_callback方法给test_compute加一个回调函数get_result,而get_result函数中通过future.result方法获取协程的返回值。

总结

asyncio使用了与以往python用法完全不同的构造:事件循环、协程和futures。这给我们的日常学习和使用增加了难度,但是由于协程提供了相较于线程更加安全的使用方式和并不逊色的性能,使得asyncio的应用前景非常广阔(本文中提到了很多的模块已经在支持asyncio),喜欢python的你,怎么能不认真学习一下呢?

最后,提供一个用Python和aiohttp创建RESTful API的小例子:

示例6

from aiohttp import web
import json

async def handle(request):
   response_obj = {'status': 'success'}
   return web.Response(text=json.dumps(response_obj))

async def new_user(request):
   try:
       # 获取url中的name值
       user = request.query['name']
       # 模拟创建了一个用户
       print("Creating new user with name: ", user)
       # 状态为200时返回的内容
       response_obj = {'status': 'success'}
       return web.Response(text=json.dumps(response_obj), status=200)
   except Exception as e:
       response_obj = {'status': 'failed', 'reason': str(e)}
       return web.Response(text=json.dumps(response_obj), status=500)

app = web.Application()
# 添加路由
app.router.add_get('/', handle)
app.router.add_get('/user', new_user)

web.run_app(app)

运行代码,在浏览器中输入: http://localhost:8080/ 页面可以看到如下输出表示成功:

{"status": "success"}

输入: http://localhost:8080/user?name=wumingxiaoyao 后台可以看到:

======== Running on http://0.0.0.0:8080 ========
(Press CTRL+C to quit)
Creating new user with name:  wumingxiaoyao


如果文章对你有帮助,可对作者进行小额赞赏


关注公众号,订阅Python技术


相关阅读:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK