过去几年,异步编程方式被越来越多的程序员使用, 当然这是有原因的。 尽管异步编程比顺序编程更难, 但是它也更高效。
在顺序编程中, 发起一个HTTP请求需要阻塞以等待他的返回结果, 使用异步编程你可以发起这个HTTP请求, 然后在等待结果返回的同时做一些其他的事情,等待结果的协程会被放在一个队列里面。 为了保证逻辑的正确性, 这可能会需要考虑的更多, 但是这也使我们用更少的资源处理更多的事情。
Python中的异步语法和调用并不难。 和Javascript的异步编程比起来可能有点难, 不过也还好。
异步的处理方式可能解释了为什么Node.js在服务器端这么流行。 我们的代码依赖于外部的一些资源, 尤其是IO密集型的应用, 比如网站可能需要从数据库调用中向一个REST接口POST数据。 一旦我们请求一些外部资源, 我们的代码就要阻塞, 而不能处理别的逻辑。
利用异步编程, 我们可以在等待其他资源返回的时候, 做一些其他的事情。
Coroutines
在python中,异步函数被称作协程: 使用async关键字 或者利用@asyncio.coroutine装饰器。 下面的两种形式是等效的:
import asyncio
async def ping_server(ip):
pass
@asyncio.coroutine
def load_file(path):
pass
上面的函数调用的时候返回的是一个协程的对象。 如果你熟悉javascript, 你可以认为这个返回对象就像javascript里面的Promise。 现在调用这两个函数, 是不能执行的, 仅仅返回的是一个协程对象, 这个对象可以被用来在后面的event loop中使用。
如果你想知道一个函数是不是协程, asyncio提供的asyncio.iscoroutine(obj)函数可以帮助你。
Yield from
有几种调用协程的方式,其中一种是使用yield from方法。 yield from在Python3.3中被引进, 在Python3.5的async/await(我们后面会提到) 得到进一步的扩展。
yield from表达式可以用如下的方式使用:
import asyncio
@asyncio.coroutine
def get_json(client, url):
file_content = yield from load_file('/Users/scott/data.txt')
如上, yield from在有@asyncio.coroutine装饰器的函数中使用的示例。 如果你在函数外面使用yield from, 你会得到下面的错误:
File "main.py", line 1
file_content = yield from load_file('/Users/scott/data.txt')
^
SyntaxError: 'yield' outside function
必须在函数中使用yield from, 典型的用法是在有@asyncio.coroutine装饰器的函数种使用。
Async/await
更新、更方便的语法是使用async/await关键字。async关键字是在Python3.5引入的, 被用来修饰一个函数, 让其成为协程, 和@asyncio.coroutine功能类似。 使用如下:
async def ping_server(ip):
# ping code here...
调用这个函数, 使用await, 而不是yield from, 不过方式差不多:
async def ping_local():
return await ping_server('192.168.1.1')
你不能在一个协程外面使用await关键字, 否则会得到语法错误。 就像yield from不能在函数外面使用一样。
Python3.5中, 上面两种协程声明的方式都支持, 但是首选async/await方式。
Running the event loop
上面描述的协程例子都不会正常的运行, 如果要运行, 需要用到event loop.。event loop是协程执行的控制点, 如果你希望执行协程, 就需要用到它们。
event loop提供了如下的特性:
注册、执行、取消延时调用(异步函数)
创建用于通信的client和server协议(工具)
创建和别的程序通信的子进程和协议(工具)
把函数调用送入线程池中
有一些配置和event loop的类型你可以使用, 但是如果你想去执行一个函数, 可以使用下面的配置, 而且在大多数场景中这样就够了:
import asyncio
async def speak_async():
print('OMG asynchronicity!')
loop = asyncio.get_event_loop()
loop.run_until_complete(speak_async())
loop.close()
最后三行是重点。 asyncio启动默认的event loop(asyncio.get_event_loop()), 调度并执行异步任务, 关闭event loop。
loop.run_until_complete()这个函数是阻塞执行的, 直到所有的异步函数执行完毕。 因为我们的程序是单线程运行的, 所以, 它没办法调度到别的线程执行。
你可能会认为这不是很有用, 因为我们的程序阻塞在event loop上(就像IO调用), 但是想象一下这样: 我们可以把我们的逻辑封装在异步函数中, 这样你就能同时执行很多的异步请求了, 比如在一个web服务器中。
你可以把event loop放在一个单独的线程中, 让它执行IO密集型的请求, 而主线程可以继续处理程序逻辑或者UI渲染。
An example
OK, 让我看一个稍微长一点的例子, 这个例子是可以实际运行的。 例子是一个简单的从Reddit的/r/python, /r/programming, and /r/compsci页面异步获取JSON数据, 解析, 打印出这些页面发表的文章。
get_json()方法是被get_reddit_top()调用的, get_reddit_top()发起了一个HTTP GET请求到Reddit。 当调用被await修饰, event loop就会继续在等待请求返回的时候处理其他的协程。 一旦请求返回, JSON数据会被返回get_reddit_top(), 然后解析, 打印。
import signal
import sys
import asyncio
import aiohttp
import json
loop = asyncio.get_event_loop()
client = aiohttp.ClientSession(loop=loop)
async def get_json(client, url):
async with client.get(url) as response:
assert response.status == 200
return await response.read()
async def get_reddit_top(subreddit, client):
data1 = await get_json(client, 'https://www.reddit.com/r/' + subreddit + '/top.json?sort=top&t=day&limit=5')
j = json.loads(data1.decode('utf-8'))
for i in j['data']['children']:
score = i['data']['score']
title = i['data']['title']
link = i['data']['url']
print(str(score) + ': ' + title + ' (' + link + ')')
print('DONE:', subreddit + '\n')
def signal_handler(signal, frame):
loop.stop()
client.close()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
asyncio.ensure_future(get_reddit_top('python', client))
asyncio.ensure_future(get_reddit_top('programming', client))
asyncio.ensure_future(get_reddit_top('compsci', client))
loop.run_forever()
这个程序和上面展示的例子有一点不同。 我们使用asyncio.ensure_future()让event loop处理多个协程, 然后让event loop一直执行, 直到处理了所有的请求。
为了执行这个程序, 需要安装aiohttp, 你可以用pip来安装:
pip install aiohttp
要保证这个程序运行在python3.5以后的版本, 输出的结果如下:
$ python main.py
46: Python async/await Tutorial (http://stackabuse.com/python-async-await-tutorial/)
16: Using game theory (and Python) to explain the dilemma of exchanging gifts. Turns out: giving a gift probably feels better than receiving one... (http://vknight.org/unpeudemath/code/2015/12/15/The-Prisoners-Dilemma-of-Christmas-Gifts/)
56: Which version of Python do you use? (This is a poll to compare the popularity of Python 2 vs. Python 3) (http://strawpoll.me/6299023)
DONE: python
71: The Semantics of Version Control - Wouter Swierstra (http://www.staff.science.uu.nl/~swier004/Talks/vc-semantics-15.pdf)
25: Favorite non-textbook CS books (https://www.reddit.com/r/compsci/comments/3xag9e/favorite_nontextbook_cs_books/)
13: CompSci Weekend SuperThread (December 18, 2015) (https://www.reddit.com/r/compsci/comments/3xacch/compsci_weekend_superthread_december_18_2015/)
DONE: compsci
1752: 684.8 TB of data is up for grabs due to publicly exposed MongoDB databases (https://blog.shodan.io/its-still-the-data-stupid/)
773: Instagram's Million Dollar Bug? (http://exfiltrated.com/research-Instagram-RCE.php)
387: Amazingly simple explanation of Diffie-Hellman. His channel has tons of amazing videos and only a few views :( thought I would share! (https://www.youtube.com/watch?v=Afyqwc96M1Y)
DONE: programming
如果你多运行几次这个程序, 得到的输出结果是不一样的。 这是因为我们调用的协程的同时, 允许其他的HTTP请求执行。 结果最先返回的请求最先打印出来。
总结
尽管Python内置的异步函数使用起来没有Javascript中的那么简便, 不过, 这不意味着它不能使应用更有趣和高效。 花费30分钟去学习异步相关的知识, 你就能更好的把它应用在你的项目中。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。