Способ асинхронного трио, чтобы решить пример Геттингера
Раймонд Хеттингер выступил с докладом о параллелизме в 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)