Способ асинхронного трио, чтобы решить пример Геттингера

Раймонд Хеттингер выступил с докладом о параллелизме в 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 секунд.

Сегодня я нашел библиотеку трио, которая имеет довольно дружественный 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)

и второй (та же скорость):

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)

Так как же этот пример следует рассматривать с использованием трио?

2 ответа

Решение

Две вещи:

Во-первых, точка асинхронности - это параллелизм. Это не делает вещи волшебно быстрее; он просто предоставляет инструментарий для одновременного выполнения нескольких задач (что может быть быстрее, чем последовательное выполнение). Если вы хотите, чтобы что-то происходило одновременно, вы должны запросить это явно. В трио, способ сделать это, создав питомник, а затем позвонив 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)

Но если вы попытаетесь внести это изменение, а затем запустите код, вы увидите, что он все еще не быстрее. Почему бы и нет? Чтобы ответить на этот вопрос, нам нужно немного подкрепиться и понять основную идею "асинхронного" параллелизма. В асинхронном коде у нас могут быть параллельные задачи, но трио фактически выполняет только одну из них в любой момент времени. Таким образом, у вас не может быть двух задач, выполняющих что-то одновременно. НО, у вас может быть две (или более) задачи, сидящие и ожидающие одновременно. И в такой программе, как эта, большая часть времени, затрачиваемого на выполнение 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))

Заметить, что 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))

Он печатает что-то вроде:

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 ...]

Чтобы избежать этого, нам нужно переключиться на 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)

Теперь это должно работать быстрее, чем последовательная версия.

Более подробное обсуждение обоих этих пунктов в учебном пособии по трио: https://trio.readthedocs.io/en/latest/tutorial.html

Вы также можете найти этот доклад полезным: 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)
Другие вопросы по тегам