Я плюсовать не могу:) Это точно, что-то когда-то пошло не так. Меня тут напрягают несколько моментов
дихотомия sync vs async миров -- из-за разных путей вызовов магических методов в кишках очень сложно писать код, который работал бы и там, и там без копипасты. Мне кажется, если бы одни и те же dunder методы можно было вызывать и в sync режим, и в async -- питон был бы сильно проще. И мне кажется, что корень зла -- генераторное прошлое корутин. Т.е. их сначала сделали, извернувшись, через генераторы, но потом закопали эти генераторы глубоко в имплементацию, добавив async/await сахар поверх, а мб стоило выкинуть генераторы и сделать async/await "на уровне интерпретатора". Но я глубоко про это не думал, это, очевидно, сложная тема + задним числом все умные, как всегда
особняком стоит гениальный и настолько же взрывоопасный gevent, который позволяет не разделять миры, но делает это настолько инвазивно, что сидишь все время гадаешь, бомбанет или нет и главное -- в каком месте
зоопарк несовместимых друг с другом ивентлупов и завязанность третьесторонних библиотек на конкретные имплементации
жару добавляет какая-нибудь реализация gRPC на питоне -- python thread-based сервер поверх cython-а, который поверх сишной либы, у которой свой тред пул.
и ты такой пилишь проект на джанге с gevent-ом и сидишь трясешься, не понимая, будет ли gRPC работать на greenlet-ах и можно ли и как это все поженить с какими-нибудь async-либами (у gevent и asyncio ивентлупы не 1-в-1 совместимы), а потом еще думаешь, а классно сделать в trio стиле и мозг в итоге взрывается. Слишком много слоев, которые все надо держать в голове
С generic точки зрения-то все так, но конкретно в питоне существование GIL (в CPython, который стандарт де-факто, как ни крути) нельзя игнорировать -- в python web фреймворках раннеры на тредах делают только в том случае, если код хендлеров написан не в async манере / если используются бинарные расширения (например, numpy, который вообще держит свой пулл openmp потоков под капотом), отпускающие GIL. То же самое -- использование TheadPoolExecutor не даст прироста, если не отпускать GIL в бинарных расширениях. Вообще с раннерами же правило большого пальца -- делать все на процессных воркерах -- и жизненный цикл воркеров с мастером развязан (можно киллять без зазрения совести), и утилизация cpu из коробки нормальная. Единственная проблема -- ботлнек в общении с мастер-процессом, но это мизер и там где это надо учитывать в принципе не надо писать на питоне.
С документацией одна беда -- она не user-friendly и не для начинающего входить в эту тему, для меня ее полезность стала актуальной только когда я полез в исходники asyncio. На своем курсе по Python я активно рекламирую цикл уроков от bbc https://bbc.github.io/cloudfit-public-docs/ и вишенкой на торте -- шикарнейшие видеокасты от David Beazley, который пишет свой микро asyncio -- после этого наступает просветление и кристалльное понимание, что никакой магии там не происходит. После -- разбор концепций структурного асинхронного программирования (trio / anyio), потом депрессия от понимания, что асинхронность в Python безвозвратно продолбана и наконец -- посыл Python к чертовой матери и переход на Go =D
Глаз зацепился, даже аккаунт полез восстанавливать. Все же, стоило нормально разобраться, что там с выигрышем по времени и отчего он происходит, а отчего нет. В итоге статья получилась не только простенькая, но еще и неправильная. А при нормальной проработке вопросов получилась бы конфета.
Не дожидаясь ответа, управление потоком отдается снова event loop'у, который создает следующую по "for циклу" корутину, которая тоже отправляет запрос.
В вашем первом примере с async-await-oм ничего такого не происходит. Следующая итерация цикла не начнется, до тех пор, пока мы не вывалимся (последовательно) из обоих await-ов.
await coroutine -- синтаксический сахар над await from iterable. В этот момент передачи управления event-loop-у не происходит -- мы сразу проваливаемся в корутину
корутина session.get() через цепочку await-ов внутри себя в какой-то момент блокируется (например, на ожидание сокета). Все это происходит через asyncio методы, поэтому он про все знает -- и помечает всю текущую task как ожидающую сокет (там все немного интереснее, но верхнеуровнево -- так), все это дело засыпает и контроль передается event-loop-у
В event-loop-е в этот момент нет никаких готовых к запуску здесь и сейчас task (есть всего одна и та в режиме ожидания)
Когда сокет готов, asyncio передает управление таске -- мы вылетаем из await-а
так, последовательно мы доходим до конца итерации цикла и начинаем следующую -- только тогда и начнем второе хождение по http
Доп. моменты, которые стоило бы обсудить:
Все, таки, что с тредингом и multiprocesing -- упомянуто три четыре способа, один из которых неправильный, а два -- "ну это пропустим". Multiprocessing вполне себе валидный инструмент в определенных контекстах.
rate limit -- подумали бы, как сделать красивое ограничение на частоту запросов
какие минусы вашего решения, если нужно обкачать много-много запросов, но с низким рейт лимитом (подсказка -- в event-loop будут создаваться ненужные task-и)
как поступать в таком случае (подсказка -- asyncio очередь с воркерами)
Выглядит исключительно годно, спасибо!
Я плюсовать не могу:) Это точно, что-то когда-то пошло не так. Меня тут напрягают несколько моментов
дихотомия sync vs async миров -- из-за разных путей вызовов магических методов в кишках очень сложно писать код, который работал бы и там, и там без копипасты. Мне кажется, если бы одни и те же dunder методы можно было вызывать и в sync режим, и в async -- питон был бы сильно проще. И мне кажется, что корень зла -- генераторное прошлое корутин. Т.е. их сначала сделали, извернувшись, через генераторы, но потом закопали эти генераторы глубоко в имплементацию, добавив async/await сахар поверх, а мб стоило выкинуть генераторы и сделать async/await "на уровне интерпретатора". Но я глубоко про это не думал, это, очевидно, сложная тема + задним числом все умные, как всегда
особняком стоит гениальный и настолько же взрывоопасный gevent, который позволяет не разделять миры, но делает это настолько инвазивно, что сидишь все время гадаешь, бомбанет или нет и главное -- в каком месте
есть изыски типа https://sobolevn.me/2020/06/how-async-should-have-been , идеи интересные, но я считаю, что такое в прод тащить нельзя =D
зоопарк несовместимых друг с другом ивентлупов и завязанность третьесторонних библиотек на конкретные имплементации
жару добавляет какая-нибудь реализация gRPC на питоне -- python thread-based сервер поверх cython-а, который поверх сишной либы, у которой свой тред пул.
и ты такой пилишь проект на джанге с gevent-ом и сидишь трясешься, не понимая, будет ли gRPC работать на greenlet-ах и можно ли и как это все поженить с какими-нибудь async-либами (у gevent и asyncio ивентлупы не 1-в-1 совместимы), а потом еще думаешь, а классно сделать в trio стиле и мозг в итоге взрывается. Слишком много слоев, которые все надо держать в голове
С generic точки зрения-то все так, но конкретно в питоне существование GIL (в CPython, который стандарт де-факто, как ни крути) нельзя игнорировать -- в python web фреймворках раннеры на тредах делают только в том случае, если код хендлеров написан не в async манере / если используются бинарные расширения (например, numpy, который вообще держит свой пулл openmp потоков под капотом), отпускающие GIL. То же самое -- использование TheadPoolExecutor не даст прироста, если не отпускать GIL в бинарных расширениях. Вообще с раннерами же правило большого пальца -- делать все на процессных воркерах -- и жизненный цикл воркеров с мастером развязан (можно киллять без зазрения совести), и утилизация cpu из коробки нормальная. Единственная проблема -- ботлнек в общении с мастер-процессом, но это мизер и там где это надо учитывать в принципе не надо писать на питоне.
Со всем остальным согласен :+1:
курс в (к сожалению, закрытом недавно) ОзонМастерс
С документацией одна беда -- она не user-friendly и не для начинающего входить в эту тему, для меня ее полезность стала актуальной только когда я полез в исходники asyncio. На своем курсе по Python я активно рекламирую цикл уроков от bbc https://bbc.github.io/cloudfit-public-docs/ и вишенкой на торте -- шикарнейшие видеокасты от David Beazley, который пишет свой микро asyncio -- после этого наступает просветление и кристалльное понимание, что никакой магии там не происходит. После -- разбор концепций структурного асинхронного программирования (trio / anyio), потом депрессия от понимания, что асинхронность в Python безвозвратно продолбана и наконец -- посыл Python к чертовой матери и переход на Go =D
Глаз зацепился, даже аккаунт полез восстанавливать. Все же, стоило нормально разобраться, что там с выигрышем по времени и отчего он происходит, а отчего нет. В итоге статья получилась не только простенькая, но еще и неправильная. А при нормальной проработке вопросов получилась бы конфета.
В вашем первом примере с async-await-oм ничего такого не происходит. Следующая итерация цикла не начнется, до тех пор, пока мы не вывалимся (последовательно) из обоих await-ов.
await coroutine -- синтаксический сахар над await from iterable. В этот момент передачи управления event-loop-у не происходит -- мы сразу проваливаемся в корутину
корутина session.get() через цепочку await-ов внутри себя в какой-то момент блокируется (например, на ожидание сокета). Все это происходит через asyncio методы, поэтому он про все знает -- и помечает всю текущую task как ожидающую сокет (там все немного интереснее, но верхнеуровнево -- так), все это дело засыпает и контроль передается event-loop-у
В event-loop-е в этот момент нет никаких готовых к запуску здесь и сейчас task (есть всего одна и та в режиме ожидания)
Когда сокет готов, asyncio передает управление таске -- мы вылетаем из await-а
так, последовательно мы доходим до конца итерации цикла и начинаем следующую -- только тогда и начнем второе хождение по http
Доп. моменты, которые стоило бы обсудить:
Все, таки, что с тредингом и multiprocesing -- упомянуто три четыре способа, один из которых неправильный, а два -- "ну это пропустим". Multiprocessing вполне себе валидный инструмент в определенных контекстах.
rate limit -- подумали бы, как сделать красивое ограничение на частоту запросов
какие минусы вашего решения, если нужно обкачать много-много запросов, но с низким рейт лимитом (подсказка -- в event-loop будут создаваться ненужные task-и)
как поступать в таком случае (подсказка -- asyncio очередь с воркерами)