解决 Hettinger 示例的异步三重奏方法
async trio way to solve Hettinger's example
Raymond Hettinger gave a talk on concurrency 在 python 中,其中一个示例如下所示:
import urllib.request
sites = [
'https://www.yahoo.com/',
'http://www.cnn.com',
'http://www.python.org',
'http://www.jython.org',
'http://www.pypy.org',
'http://www.perl.org',
'http://www.cisco.com',
'http://www.facebook.com',
'http://www.twitter.com',
'http://www.macrumors.com/',
'http://arstechnica.com/',
'http://www.reuters.com/',
'http://abcnews.go.com/',
'http://www.cnbc.com/',
]
for url in sites:
with urllib.request.urlopen(url) as u:
page = u.read()
print(url, len(page))
基本上我们会追踪这些链接并打印接收到的字节数,运行 大约需要 20 秒。
今天我发现 trio 库非常友好 api。但是当我尝试将它与这个相当基本的示例一起使用时,我做错了。
第一次尝试(运行s 大约相同的 20 秒):
import urllib.request
import trio, time
sites = [
'https://www.yahoo.com/',
'http://www.cnn.com',
'http://www.python.org',
'http://www.jython.org',
'http://www.pypy.org',
'http://www.perl.org',
'http://www.cisco.com',
'http://www.facebook.com',
'http://www.twitter.com',
'http://www.macrumors.com/',
'http://arstechnica.com/',
'http://www.reuters.com/',
'http://abcnews.go.com/',
'http://www.cnbc.com/',
]
async def show_len(sites):
t1 = time.time()
for url in sites:
with urllib.request.urlopen(url) as u:
page = u.read()
print(url, len(page))
print("code took to run", time.time() - t1)
if __name__ == "__main__":
trio.run(show_len, sites)
第二个(速度相同):
import urllib.request
import trio, time
sites = [
'https://www.yahoo.com/',
'http://www.cnn.com',
'http://www.python.org',
'http://www.jython.org',
'http://www.pypy.org',
'http://www.perl.org',
'http://www.cisco.com',
'http://www.facebook.com',
'http://www.twitter.com',
'http://www.macrumors.com/',
'http://arstechnica.com/',
'http://www.reuters.com/',
'http://abcnews.go.com/',
'http://www.cnbc.com/',
]
async def link_user(url):
with urllib.request.urlopen(url) as u:
page = u.read()
print(url, len(page))
async def show_len(sites):
t1 = time.time()
for url in sites:
await link_user(url)
print("code took to run", time.time() - t1)
if __name__ == "__main__":
trio.run(show_len, sites)
那么应该如何使用 trio 来处理这个例子?
两件事:
首先async的重点是并发。它不会神奇地让事情变得更快;它只是提供了一个同时做多件事的工具包(这可能比按顺序做更快)。如果您希望事情同时发生,那么您需要明确提出请求。在 trio 中,你这样做的方法是创建一个 nursery,然后调用它的 start_soon
方法。例如:
async def show_len(sites):
t1 = time.time()
async with trio.open_nursery() as nursery:
for url in sites:
nursery.start_soon(link_user, url)
print("code took to run", time.time() - t1)
但是,如果您尝试进行此更改然后 运行 修改代码,您会发现它仍然 并不快。为什么不?要回答这个问题,我们需要稍微回顾一下并了解 "async" 并发的基本概念。在异步代码中,我们可以有并发任务,但 trio 实际上在任何给定时间只有 运行s 其中之一。所以你不能有两个任务实际上同时做某事。但是,您可以同时有两个(或更多)任务坐等。在这样的程序中,花在 HTTP 请求上的大部分时间都花在等待响应返回上,因此可以通过使用并发任务来加速:我们启动所有任务,然后他们每个人 运行s 一段时间发送请求,停止等待响应,然后在等待下一个 运行s 一段时间,发送请求,停止等待等待它的响应,然后在等待 下一个 一个 运行 时...你明白了。
好吧,实际上,在 Python 中,到目前为止我所说的一切也适用于线程,因为 GIL 意味着即使你有多个线程,实际上也只有一个 运行一次宁。
在 Python 中,异步并发和 thread-based 并发之间的最大区别在于,在 thread-based 并发中,解释器可以随时暂停任何线程并切换到 运行宁另一个线程。在异步并发中,我们只在源代码中标记的特定点在任务之间切换——这就是 await
关键字的作用,它向您显示任务可能暂停的位置以让另一个任务 运行 .这样做的好处是它使您的程序更容易推理,因为不同的 threads/tasks 交错和意外相互干扰的方式要少得多。缺点是可能会编写不在正确位置使用 await
的代码,这意味着我们 不能 切换到另一个任务。特别是,如果我们停下来等待某事,但没有用 await
标记它,那么我们的整个程序都会停止,而不仅仅是发出阻塞调用的特定任务。
现在让我们再次查看您的示例代码:
async def link_user(url):
with urllib.request.urlopen(url) as u:
page = u.read()
print(url, len(page))
请注意 link_user
根本没有使用 await
。这就是让我们的程序从 运行ning 并发停止的原因:每次我们调用 link_user
时,它都会发送请求,然后等待响应,而不让任何其他事情发生 运行.
如果在开头添加一些打印调用,您会更容易看到这一点:
async def link_user(url):
print("starting to fetch", url)
with urllib.request.urlopen(url) as u:
page = u.read()
print("finished fetching", url, len(page))
它打印出如下内容:
starting to fetch https://www.yahoo.com/
finished fetching https://www.yahoo.com/ 520675
starting to fetch http://www.cnn.com
finished fetching http://www.cnn.com 171329
starting to fetch http://www.python.org
finished fetching http://www.python.org 49239
[... you get the idea ...]
为避免这种情况,我们需要切换到专为 trio 设计的 HTTP 库。希望将来我们会有像 urllib3 and requests. Until then, your best choice is probably asks.
这样熟悉的选项
因此,您的代码重写为 运行 link_user
并发调用,并使用异步 HTTP 库:
import trio, time
import asks
asks.init("trio")
sites = [
'https://www.yahoo.com/',
'http://www.cnn.com',
'http://www.python.org',
'http://www.jython.org',
'http://www.pypy.org',
'http://www.perl.org',
'http://www.cisco.com',
'http://www.facebook.com',
'http://www.twitter.com',
'http://www.macrumors.com/',
'http://arstechnica.com/',
'http://www.reuters.com/',
'http://abcnews.go.com/',
'http://www.cnbc.com/',
]
async def link_user(url):
print("starting to fetch", url)
r = await asks.get(url)
print("finished fetching", url, len(r.content))
async def show_len(sites):
t1 = time.time()
async with trio.open_nursery() as nursery:
for url in sites:
nursery.start_soon(link_user, url)
print("code took to run", time.time() - t1)
if __name__ == "__main__":
trio.run(show_len, sites)
现在这应该 运行 比顺序版本快。
三重奏教程中对这两点有更多讨论:https://trio.readthedocs.io/en/latest/tutorial.html#async-functions
您可能还会发现此演讲有用:https://www.youtube.com/watch?v=i-R704I8ySE
使用 httpx 的异步示例,它与 asyncio
和 trio
都兼容,并且具有与 requests
.
非常相似的界面
import trio, time
import httpx
sites = [
'https://www.yahoo.com/',
'https://www.cnn.com',
'https://www.python.org',
'https://www.jython.org',
'https://www.pypy.org',
'https://www.perl.org',
'https://www.cisco.com',
'https://www.facebook.com',
'https://www.twitter.com',
'https://www.macrumors.com/',
'https://arstechnica.com/',
'https://www.reuters.com/',
'https://abcnews.go.com/',
'https://www.cnbc.com/',
]
async def link_user(url):
print("starting to fetch", url)
async with httpx.AsyncClient() as client:
r = await client.get(url)
print("finished fetching", url, len(r.content))
async def show_len(sites):
t1 = time.time()
async with trio.open_nursery() as nursery:
for url in sites:
nursery.start_soon(link_user, url)
print("code took to run", time.time() - t1)
if __name__ == "__main__":
trio.run(show_len, sites)
Raymond Hettinger gave a talk on concurrency 在 python 中,其中一个示例如下所示:
import urllib.request
sites = [
'https://www.yahoo.com/',
'http://www.cnn.com',
'http://www.python.org',
'http://www.jython.org',
'http://www.pypy.org',
'http://www.perl.org',
'http://www.cisco.com',
'http://www.facebook.com',
'http://www.twitter.com',
'http://www.macrumors.com/',
'http://arstechnica.com/',
'http://www.reuters.com/',
'http://abcnews.go.com/',
'http://www.cnbc.com/',
]
for url in sites:
with urllib.request.urlopen(url) as u:
page = u.read()
print(url, len(page))
基本上我们会追踪这些链接并打印接收到的字节数,运行 大约需要 20 秒。
今天我发现 trio 库非常友好 api。但是当我尝试将它与这个相当基本的示例一起使用时,我做错了。
第一次尝试(运行s 大约相同的 20 秒):
import urllib.request
import trio, time
sites = [
'https://www.yahoo.com/',
'http://www.cnn.com',
'http://www.python.org',
'http://www.jython.org',
'http://www.pypy.org',
'http://www.perl.org',
'http://www.cisco.com',
'http://www.facebook.com',
'http://www.twitter.com',
'http://www.macrumors.com/',
'http://arstechnica.com/',
'http://www.reuters.com/',
'http://abcnews.go.com/',
'http://www.cnbc.com/',
]
async def show_len(sites):
t1 = time.time()
for url in sites:
with urllib.request.urlopen(url) as u:
page = u.read()
print(url, len(page))
print("code took to run", time.time() - t1)
if __name__ == "__main__":
trio.run(show_len, sites)
第二个(速度相同):
import urllib.request
import trio, time
sites = [
'https://www.yahoo.com/',
'http://www.cnn.com',
'http://www.python.org',
'http://www.jython.org',
'http://www.pypy.org',
'http://www.perl.org',
'http://www.cisco.com',
'http://www.facebook.com',
'http://www.twitter.com',
'http://www.macrumors.com/',
'http://arstechnica.com/',
'http://www.reuters.com/',
'http://abcnews.go.com/',
'http://www.cnbc.com/',
]
async def link_user(url):
with urllib.request.urlopen(url) as u:
page = u.read()
print(url, len(page))
async def show_len(sites):
t1 = time.time()
for url in sites:
await link_user(url)
print("code took to run", time.time() - t1)
if __name__ == "__main__":
trio.run(show_len, sites)
那么应该如何使用 trio 来处理这个例子?
两件事:
首先async的重点是并发。它不会神奇地让事情变得更快;它只是提供了一个同时做多件事的工具包(这可能比按顺序做更快)。如果您希望事情同时发生,那么您需要明确提出请求。在 trio 中,你这样做的方法是创建一个 nursery,然后调用它的 start_soon
方法。例如:
async def show_len(sites):
t1 = time.time()
async with trio.open_nursery() as nursery:
for url in sites:
nursery.start_soon(link_user, url)
print("code took to run", time.time() - t1)
但是,如果您尝试进行此更改然后 运行 修改代码,您会发现它仍然 并不快。为什么不?要回答这个问题,我们需要稍微回顾一下并了解 "async" 并发的基本概念。在异步代码中,我们可以有并发任务,但 trio 实际上在任何给定时间只有 运行s 其中之一。所以你不能有两个任务实际上同时做某事。但是,您可以同时有两个(或更多)任务坐等。在这样的程序中,花在 HTTP 请求上的大部分时间都花在等待响应返回上,因此可以通过使用并发任务来加速:我们启动所有任务,然后他们每个人 运行s 一段时间发送请求,停止等待响应,然后在等待下一个 运行s 一段时间,发送请求,停止等待等待它的响应,然后在等待 下一个 一个 运行 时...你明白了。
好吧,实际上,在 Python 中,到目前为止我所说的一切也适用于线程,因为 GIL 意味着即使你有多个线程,实际上也只有一个 运行一次宁。
在 Python 中,异步并发和 thread-based 并发之间的最大区别在于,在 thread-based 并发中,解释器可以随时暂停任何线程并切换到 运行宁另一个线程。在异步并发中,我们只在源代码中标记的特定点在任务之间切换——这就是 await
关键字的作用,它向您显示任务可能暂停的位置以让另一个任务 运行 .这样做的好处是它使您的程序更容易推理,因为不同的 threads/tasks 交错和意外相互干扰的方式要少得多。缺点是可能会编写不在正确位置使用 await
的代码,这意味着我们 不能 切换到另一个任务。特别是,如果我们停下来等待某事,但没有用 await
标记它,那么我们的整个程序都会停止,而不仅仅是发出阻塞调用的特定任务。
现在让我们再次查看您的示例代码:
async def link_user(url):
with urllib.request.urlopen(url) as u:
page = u.read()
print(url, len(page))
请注意 link_user
根本没有使用 await
。这就是让我们的程序从 运行ning 并发停止的原因:每次我们调用 link_user
时,它都会发送请求,然后等待响应,而不让任何其他事情发生 运行.
如果在开头添加一些打印调用,您会更容易看到这一点:
async def link_user(url):
print("starting to fetch", url)
with urllib.request.urlopen(url) as u:
page = u.read()
print("finished fetching", url, len(page))
它打印出如下内容:
starting to fetch https://www.yahoo.com/
finished fetching https://www.yahoo.com/ 520675
starting to fetch http://www.cnn.com
finished fetching http://www.cnn.com 171329
starting to fetch http://www.python.org
finished fetching http://www.python.org 49239
[... you get the idea ...]
为避免这种情况,我们需要切换到专为 trio 设计的 HTTP 库。希望将来我们会有像 urllib3 and requests. Until then, your best choice is probably asks.
这样熟悉的选项因此,您的代码重写为 运行 link_user
并发调用,并使用异步 HTTP 库:
import trio, time
import asks
asks.init("trio")
sites = [
'https://www.yahoo.com/',
'http://www.cnn.com',
'http://www.python.org',
'http://www.jython.org',
'http://www.pypy.org',
'http://www.perl.org',
'http://www.cisco.com',
'http://www.facebook.com',
'http://www.twitter.com',
'http://www.macrumors.com/',
'http://arstechnica.com/',
'http://www.reuters.com/',
'http://abcnews.go.com/',
'http://www.cnbc.com/',
]
async def link_user(url):
print("starting to fetch", url)
r = await asks.get(url)
print("finished fetching", url, len(r.content))
async def show_len(sites):
t1 = time.time()
async with trio.open_nursery() as nursery:
for url in sites:
nursery.start_soon(link_user, url)
print("code took to run", time.time() - t1)
if __name__ == "__main__":
trio.run(show_len, sites)
现在这应该 运行 比顺序版本快。
三重奏教程中对这两点有更多讨论:https://trio.readthedocs.io/en/latest/tutorial.html#async-functions
您可能还会发现此演讲有用:https://www.youtube.com/watch?v=i-R704I8ySE
使用 httpx 的异步示例,它与 asyncio
和 trio
都兼容,并且具有与 requests
.
import trio, time
import httpx
sites = [
'https://www.yahoo.com/',
'https://www.cnn.com',
'https://www.python.org',
'https://www.jython.org',
'https://www.pypy.org',
'https://www.perl.org',
'https://www.cisco.com',
'https://www.facebook.com',
'https://www.twitter.com',
'https://www.macrumors.com/',
'https://arstechnica.com/',
'https://www.reuters.com/',
'https://abcnews.go.com/',
'https://www.cnbc.com/',
]
async def link_user(url):
print("starting to fetch", url)
async with httpx.AsyncClient() as client:
r = await client.get(url)
print("finished fetching", url, len(r.content))
async def show_len(sites):
t1 = time.time()
async with trio.open_nursery() as nursery:
for url in sites:
nursery.start_soon(link_user, url)
print("code took to run", time.time() - t1)
if __name__ == "__main__":
trio.run(show_len, sites)