异步三重奏解决Hettinger的例子

Gra*_*der 8 python asynchronous python-trio

雷蒙德赫廷杰对并发性的演讲在蟒蛇,其中的一个例子看上去像是:

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))
Run Code Online (Sandbox Code Playgroud)

基本上我们追求这些链接并打印接收字节的数量,运行大约需要20秒.

今天我发现三人图书馆有相当友好的api.但是,当我试图在这个相当基本的例子中使用它时,我没有做对.

第一次尝试(运行大约相同的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)
Run Code Online (Sandbox Code Playgroud)

和第二个(相同的速度):

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)
Run Code Online (Sandbox Code Playgroud)

那么这个例子应该用三重奏来处理呢?

Nat*_*ith 21

两件事情:

首先,异步的重点是并发.它不会让事情神奇地变得更快; 它只是提供了一个工具包,可以同时执行多项操作(这可能比按顺序执行更快).如果您希望事情同时发生,那么您需要明确请求.在三重奏中,你这样做的方法是创建一个托儿所,然后调用它的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)
Run Code Online (Sandbox Code Playgroud)

但是,如果您尝试进行此更改然后运行代码,您将看到它仍然没有更快.为什么不?要回答这个问题,我们需要稍微备份并理解"异步"并发的基本思想.在异步代码中,我们可以有并发任务,但三重奏实际上只在任何给定时间运行其中一个.所以你不能让两个任务同时做某事.但是,您可以同时进行两个(或更多)任务并等待.在这样的程序中,花在做HTTP请求上的大部分时间花在坐着等待响应回来,这样就可以通过使用并发任务来获得加速:我们启动所有任务,然后他们每个人运行一段时间发送请求,停止等待响应,然后在等待下一个运行一段时间,发送请求,停止等待其响应,然后等待它一个运行......你明白了.

嗯,实际上,在Python中,我到目前为止所说的所有内容也适用于线程,因为GIL意味着即使你有多个线程,一次只能运行一个线程.

在Python中,异步并发和基于线程的并发之间的最大区别在于,在基于线程的并发中,解释器可以随时暂停任何线程并切换到运行另一个线程.在异步并发中,我们只在源代码中标记的特定点处的任务之间切换 - 这就是await关键字的用途,它显示了任务可能暂停的位置以让另一个任务运行.这样做的好处是它可以更容易地推理您的程序,因为不同的线程/任务可以交错并且意外地相互干扰的方式更少.缺点是可以编写不在await正确位置使用的代码,这意味着我们无法切换到另一个任务.特别是,如果我们停下来等待某些东西,但是没有标记它await,那么我们的整个程序将停止,而不仅仅是阻塞调用的特定任务.

现在让我们再看一下你的示例代码:

async def link_user(url):
    with urllib.request.urlopen(url) as u:
        page = u.read()
        print(url, len(page))
Run Code Online (Sandbox Code Playgroud)

请注意,link_user根本不使用await.这就是阻止我们的程序同时运行的原因:每次调用时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))
Run Code Online (Sandbox Code Playgroud)

它打印的内容如下:

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 ...]
Run Code Online (Sandbox Code Playgroud)

为了避免这种情况,我们需要切换到一个专门用于三重奏的HTTP库.希望将来我们会有熟悉的选项,如urllib3请求.在那之前,你最好的选择可能是要求.

所以这里是你的代码被重写以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)
Run Code Online (Sandbox Code Playgroud)

现在这应该比顺序版本运行得更快.

在三重奏教程中有更多关于这两点的讨论:https://trio.readthedocs.io/en/latest/tutorial.html#async-functions

您可能还会发现此演讲很有用:https://www.youtube.com/watch?v = -R704I8ySE