Comments 34
Только начал читать и тут такое:
что делать, когда необходимо получить некие данные в процессе выполнения определенного блока кода или до? Тут на помощь нам и приходит асинхронная обработка
Ну так вызови функцию получения этих данных "в процессе или до". Какие проблемы? Совершенно не то объяснение необходимости асинхронности. Прям даже не знаю, читать ли дальше.
Базовая база. Асинхронность имеет свою область применения (I/O задачи), независимо от языка. Когда Python зашёл в веб-серверы, пришлось догонять Node.js, где этот самый event loop в самой основе. Вот и затащили asyncio в стандартную либу. Точнее, прилепили сбоку. Если сравнивать с JavaScript, то в Python корутины как-то переусложнены. Если взять aiohttp то уже норм. Если добавить Jinja то и про Flask можно забыть. Иногда прям удобно что это чистый веб-сервер, без ASGI. А FastAPI это уже другая история, там собственно за обработку запросов отвечает незаслуженно обделённый вниманием Starlette.
Такие статьи - лучшая антиреклама курсов Слёрма. Какой-то обрывочный хаос с постоянным перепрыгиванием с темы на тему. И даже отступы в коде не проставлены. Это максимально пофигистичное отношение к качеству.
Потоки неплохо применяются только при решении задач, связанных с I/O (а именно при работе с файлами, сетью). Для остальных случаев не актуальны.
неправда, если взять CPU-bound задачи, с высокой вероятностью вы будете использовать для этого нативные библиотеки, которые отпускают GIL, потоки будут всё ещё эффективны. Плюс есть задачи не связанные с I/O но связанные с ожиданием событий - тут тоже потоки отлично справляются
При использовании потоков имеется риск состояний гонки, обязательна синхронизация.
при любой форме конкурентости это требуется. Что с asyncio, что с тредами
Из-за параллелизма усложняется процесс отладки.
любая форма конкурентости усложняет отладку.
Код в asyncio остаётся последовательным, но требует применения async/await и, в ряде случаев, дополнительных библиотек
Неправда. Последовательный код остается последовательным будь он запущен в треде или в таске. А вот если вы запускаете новые треды, новые таски, используете executor, gather или ещё что - код перестает быть последовательным. Это особенность вышего кода, а не подхода к конкуретности.
Добавлю так же что asyncio != асинхронность. Асинхронность - когда несколько событий (например завершения выполнения) происходят без фиксированного порядка. Треды и asyncio - способы этого достичь. Но да, в бытовой речи под асинком обычно подразумеваем async def функции и соответсвенно асинкио
А что Вы понимаете под "запусканием новых тасков"? Если Вы внутри цикла будете создавать новую таску на каждой итерации и await
ить ее там же (собсно, await
приводит к запуску таски), то весь ваш цикл будет полностью синхронным, потому что await
будет блокировать каждую итерацию цикла, дожидаясь завершения таски в рамках этой итерации.
Для того, чтобы этого избежать, можно а) создать таску, б) "в ручную" переключить event-loop на следующую его итерацию. Но если забыть сделать последнее (и при условии что внутри вашего цикла нет других операций, переключающихся на ивент-луп), то все ваши таски просто будут лежать мертвым грузом внутри ready
очереди ивент-лупа в статусе Pending.
Другая проблема тут - обработка ошибок в тасках. Если мы НЕ await
им нашу таску явно, то у нас и нет контроля над ее выполнением, следовательно, обработать ошибку (если она случилась внутри таски) явным способом (через try
/ except
) мы не можем (тут спасают TaskGroup
ы)
Имхо, в этом заключается самый большой косяк реализации асинхронности в питончике:
без дополнительных "приблуд" типа
gather
,wait
илиTaskGroup
вы не получите желаемой асинхронности в принципе - у вас будет синхронный код сasync / await
операции типа
asyncio.create_task
/TaskGroup().create_task
лишь создают таску и добавляют ее в очередь выполнения, но не запускают ее
Если Вы внутри цикла будете создавать новую таску на каждой итерации и
await
ить ее там же (собсно,await
приводит к запуску таски), то весь ваш цикл будет полностью синхронным
Если я внутри цикла буду запускать Thread и делать join код так же будет синхронным.
Верно, только поток запускается методом start(), а у таски попросту нет аналогичного метода
Если так рассуждать, то таска - это запуск корутины.
И сейчас мы возвращаемся в начальную точку: как запустить корутину, не дожидаясь ее результата?
так же как обычную функцию - в фоновом "потоке выпонления" (таске/треде/процессе) и просто не ждать её
так что является триггером для запуска таски/корутины?
Триггером для запуска асинк функции в отдельной таске является создание таски из корутины. Когда именно физически она будет запущена зависит от состояния потока и мыслей планировщика.
создание таски из корутины (если вы про asyncio.create_task(coro)
) не является само по себе "триггером для запуска", так как это а) создание объекта Таски б) добавление созданной Таски в очередь ивент-лупа.
Когда именно таска будет запущена - она будет запущена на следующей же итерации ивент-лупа. Чтобы переключить итерацию, вы либо явно await'ите вашу таску (что приводит к запуску ее на ивент-лупе, но и к ожиданию ее завершения), либо же вы переключаете итерацию ивент-лупа через `await asyncio.sleep(0)` (что с точки зрения эргономики языка - дичь полнейшая)
Нет никаких "мыслей планировщика" - есть конкретный алгоритм, по которому работает ивент-луп. Да и отдельного "планировщика" в питонячем asyncio тоже, по факту, нет - там все сделано " в лоб", если сравнивать с другими ЯП.
Чтобы переключить итерацию, вы либо явно await'ите вашу таску
Вы мне сейчас рассказываете разницу между кооперативной и вытесняющей многозадачностью. Она действительно есть. Но наличие кооперативности не делает конкуретный код внезапно последовательным или синхронным.
Но наличие кооперативности не делает конкуретный код внезапно последовательным или синхронным.
чтобы код был конкурентным (т.е. выполнялся конкуретно), он должен быть написан со знанием нюансов работы ивент-лупа. Наличие async/await в коде не делает его конкуретным априори, и у питонячего ивент-лупа есть конкретные ограничения. Об этом и речь.
К чему вы тут виды многозадачности привели - вообще не понятно. Некоторые языки (например, Rust) релизуют асинк-модель (например, tokio runtime) с использованием тредов, но при этом многозадачность там - кооперативная.
Наличеи async await не далает код конкурентным. Его таким делает create_task/gather. Но это такая же важная часть asyncio. Некорректно говорить, что код с использованием asyncio последовательный. Это зависит от кода - либо он запускает логические "потоки", либо не запускает. И детали работы этих "потоков" влияют на то как код будет выполняться, какие виды синхронизации нужны и т.п.
где я говорил что "код с использованием asyncio последовательный"? Я говорил про то, что код с использованием asyncio может работать синхронно, если он написан без знаний/понимания нюансов того, как работает ивент-луп. И шансы стать на эти "грабли" крайне велики, особенно для тех разработчиков, кто не сильно посвящен в детали. Причина же данной проблемы лежит в плоскости того, как именно реализован ивент-луп. Да и, в целом, асинхронная модель в пайтоне оставляет желать лучшего.
В статье было "Код в asyncio остаётся последовательным", это некорректно. Вы начали спорить с мои возражением против этого, залезая в детали как именно таски переключаются. У вас абстракции потекли
я спорил не с вашим возражением в целом, а конкретно с вашим выражением про "запуск задач" и пытался донести, что "запустить задачу" явно + при этом не блокируясь в некоторых случаях попросту нельзя
А если в целом, то будет ваш код работать реально асинхронно (a.k.a. конкурентно) или же это будет последовательный код с async/await - зависит от того, как вы его напишите
P.S. если по-честному, то данная статья вообще не стоит обсуждения ))
неправда, если взять CPU-bound задачи, с высокой вероятностью вы будете использовать для этого нативные библиотеки, которые отпускают GIL, потоки будут всё ещё эффективны
Только это будут не питонячие потоки (например, numpy
)
у питона нет никаких своих потоков, питоновский Thread создает поток вашей OS
Питоновский тред == тред, выполняющий питонячий байт-код и описываемый структурой PyThreadState. То, что там под капотом OS тред я в курсе ))
Нет такого. Есть поток OS, в котором выполняется какой-то код, в частности интерпретатор питона. Доступ к которому организован с помощью определонных биндингов.
Чего именно нет? Абстракций интерпретатора поверх OS тредов? Как, по-вашему, интерпретатор синхронизирует выполнение потоков через GIL?
Что-то не совсем понятно, с чем конкретно вы спорите )
нету своей реализации прикладных потоков. Есть asyncio с моделью выполнения 1:N (все таски в одном потоке ОС) и есть треды, соответсвенно N:N - то есть питоновский поток равен системному.
GIL - это обычный мьютекс, он влияет на то как взаимодействуют потоки, но а не являеется их альтернативной реализацией. Точно так же у вас могут быть примитивы синхронизации и нативном коде
GIL - это не «обычный мьютекс», а структура, состоящая из набора мьютексов и кондишен-переменных. И взаимодействие с ней происходит по определенному (достаточно хитрому) алгоритму.
про «альтернативную реализацию потоков в пайтоне» я нигде и не писал
Такое чувство, что вы на ходу гуглите и кидаетесь определениями, которые и так всем известны и с которыми никто и не спорил)
Вы буквально скакзали что при использовании numpy будут какие-то другие потоки. Хотя потоки у нас одни - потоки ОС. Претензия была к этому. Цитирую: "Только это будут не питонячие потоки (например, numpy
)"
Повторюсь: интерпретатор пайтона (CPython) использует свою абстракцию над потоками ОС (в комменте выше я писал какую - PyThreadState). Благодаря этому реализуется синхронизация потоков через GIL. «Физически» же выполнение пайтон байт-кода происходит в ОС потоке (иного я нигде и не утверждал). Так что «питонячий поток» - это ОС поток + PyThreadState, который использует интерпретатор. Потоки же, порождаемые С-шными либами - это тоже OC потоки, но они могут выполняться реально параллельно (на разных ядрах CPU), т.к. они вообще ничего не знают про GIL. Исходя из вышесказанного, вполне валидно логически разделять потоки на "питонячие" и (например) "с-шные", несмотря на то, что под капотом и там, и там - ОС потоки
P.S. Советую почитать очень занимательную статью за авторством Виктора Скворцова про GIL (она на английском)
Так что «питонячий поток» - это ОС поток + PyThreadState, который использует интерпретатор. Потоки же, порождаемые С-шными либами - это тоже OC потоки, но они могут выполняться реально параллельно (на разных ядрах CPU), т.к. они вообще ничего не знают про GIL.
Откуда взялись потоки порождаемые сишными либами? Я не про них. Если в потоке, порожденном питоном, отпустить GIL (а это делается в куче случаев, начиная с банального hashlib), то гил отпустится и можно занять несколько ядер.
Откуда взялись потоки порождаемые сишными либами?
оттуда же, откуда они берутся в любой библиотеке написанной на C и использующей многопоточность.
Внезапно, если в потоке порожденным питоном отпустить GIL (а это делается в куче случаев, начиная с банального hashlib), то гил отпустится и можно занять несколько ядер
спасибо за очередную порцию общеизвестных вещей) Интересно, вы сами себя пытаетесь удивить своими же познаниями?)
Вы можете написать свою собственную числодробилку на С/С++/Rust и юзать в ее реализации N потоков, а потом заиспользовать это в пайтоне. Все будет работать параллельно, потому что потоки уровня низлежащего языка не блокируются GIL (потому что они тупо не знают вообще что такое GIL).
Да хватит говорить о "потоках низлежащего языка". Если вы написали числодробилку на C (или взяли готовую), которая не порождает потоки сама, то внезано там тоже гил может быть не нужен. Такие места даже в стандартной библиотеке есть (упомянутый выше hashlib прекрасно отпускает gil). Ещё раз: НЕТ НИКАКИХ ПОТОКОВ ЯЗЫКА XXX. Есть код, которому нужен GIL или не нужен, это зависит от кода, который выполняется в потоке, а не от как поток был создан.
Еще раз: любой поток созданный в пайтон-коде будет использовать GIL. Любой поток, созданный библиотекой на С/C++/Rust, НЕ будет использовать GIL. Это попросту разные уровни абстракции. Что конкретно вам тут не понятно?
Также выше я подробно расписал, почему логическое разделение на "потоки языка X" и "потоки языка Y" вполне уместны и валидны в контексте пайтона. Подчеркиваю - ЛОГИЧЕСКОЕ, а не ФИЗИЧЕСКОЕ.
Разберитесь в вопросе для начала, чтобы понимать то, о чем вам пишут. А то уже сотый коммент просто выдаете общеизвестные истины, параллельно повторяя как мантру "нет никаких других потоков, есть только потоки ОС")
любой поток созданный в пайтон-коде будет использовать GIL.
Использовать - да, занимать - нет. Запустите в этом потоке код, который отпускает GIL и гил будет отпущен. Я не знаю к чему вы прицепились, но изначальный тезис был про то, что потоки, запущенные из питона все ещё могут работать параллельно, так как код в них может gil отпускать. В статье была некорректная формулировка, что для CPU-bound задач потоки, созданные в питоне не подходят. Подходят, с оговорками.
Скорее, это вы прицепились к моей формулировке "питонячего потока" ) Надеюсь, я выше в комментариях смог донести что именно имелось в виду.
Касательно CPU-bound задач - к сожалению, не для всего есть готовые либы, использующие "обходные пути" (отпускание GIL и т.д.). А писать свои реализации на языках без GIL / реализации, использующие Python C API - то еще удовольствие
Асинхронная обработка запросов в Python: необходимость или просто модное слово?