Как стать автором
Обновить

Комментарии 36

После прочтения сложилось впечатление, что потоки вообще не нужны. Есть ли причины по которым мне не стоит использовать asyncio для любых операций ввода/вывода?
asyncio выполняет все задачи на одном ядре. Вы не сможете запустить 10 GET запросов на 10 ядрах с помощью только цикла событий, вы обязаны создать потоки/процессы тем или иным способом для параллельного выполнения.
На сколько я понимаю, весь I/O выполняется ядром операционной системы параллельно. Event loop выступает посредником. Или нет? Если бы все было так как вы сказали, то было бы невозможно добиться одинаковой производительности async кода и потоков в питоне.
event loop выполняет все задачи в одном потоке, а значит на одном ядре процессора. Задача сама по себе может создать поток и задействовать больше ядер, но механизм event loop — однопоточный. Сравнивать async-код и потоки, вообще говоря, некорректно, это разные техники. В задачах где нужно много CPU и мало I\O — выбираем потоки, где много I\O и мало CPU — acync. Но в реальности обычно совмещают (создают потоки из async обработчиков, когда предстоит долгий CPU-расчет)
В задачах где нужно много CPU и мало I\O — выбираем потоки
А вы точно про Python говорите?
Питон — это язык, event loop — системный механизм, который можно использовать из любого языка (и из питона с помощью asyncio). Поэтому это и про питон тоже, да.
А как же GIL?
Есть много путей обхода GIL — не использовать CPython, создавать процессы вместо потоков, с питона 3.2 также пофиксили многое для многопоточки где микс CPU и IO тредов, но проблема есть да. Что касается event loop — то с ним у питона никаких проблем и ограничений нет.
А что не так с GIL? У меня встречный вопрос: как Вы думаете как выполняется блокирующее чтение файлов/сокетов в разных потоках, задействуя системные вызовы и libc? Участвует ли в этом GIL как примитив синхронизации? К примеру, 10 разных файлов в 10 разных потоках.
У меня есть ощущение, что вы не совсем поняли мой посыл. С GIL все нормально. Лок отпускается во время операции ввода-вывода, не препятствуя выполнению других потоков. Своим вопросом я выразил недоумение по поводу использования потоков для CPU bound задач в питоне.
Но ведь если сделать тот же ход (а именно нормально написать C extension, который будет отпускать GIL), то и вполне можно решать CPU bound задачи на питоне =) Понятное дело, что числодробилку на чистом CPython писать — довольно гиблое дело.
Так потоки не помогут же в том чтоб загрузить больше одного ядра. Это можно сделать только с помощью процессов в питоне.
Это справедливо только для CPython и то только для версий ниже 3.2.
Ну я думал, если не указано иного, то речь и идет о CPython.
А как потоками можно загрузить несколько ядер одновременно, если GIL не дает выполняться больше чем одному потоку в один момент времени?
GIL в определённый момент «отпускается» (например, ожидание ответа от сервера) и в этот момент может работать другой поток, т.е. задачи, что не требуют CPU. Но, насколько я знаю, потоки, всё равно, выделяются на одном ядре. Потому очень интересна информация, что потоки работают на нескольких ядрах
А как ожидание ответа от сервера загружает CPU? Загружает же обработка ответа, а эту обработку можно производить только на одном ядре одновременно.
Я криво написал) Никак не загружает, загружает составление запроса, отправка и получение ответа. В промежутках GIL отпущен и может выполняться другой поток. Но, насколько я знаю, потоки выделяются в рамках одного ядра (но это не точно). Если есть где-то информация о том, что это не так — очень хочеться почитать/посмотреть
Ещё раз простите за кривизну изложения мысли
Например asyncio не поддерживает файловый ввод-вывод.
Он посложнее в разработке и отладке.
А еще есть уйма полезных блокирующих библиотек.
Про файлы не знал, спасибо. Только это скорее операционные системы не поддерживают асинхронные операции с файлами.
Оно не работает с обычными файлами (те, что на диске).
Например, для POSIX select:
File descriptors associated with regular files shall always select true for ready to read, ready to write, and error conditions.
Так что реквестирую у вас работающий пример с чтением/записью обычного файла на диске через asyncio (без использования тред-пулов, конечно).

Можно использовать aio_read().

Т.е. вы хотите сказать, что asyicnio-вский aio_read() для дескриптора файла на диске — блокирующий?
что asyicnio-вский aio_read()
Т.е. вы хотите сказать, что asyncio использует aio? Не расскажете об этом поподробнее?
Сорян, имелся ввиду «asyncio-вский add_reader», попутал из-за коммента выше.
Нет, сам 'add_reader' не блокирует. Для select он просто сразу же (почти) вызовет коллбек, даже если реально данных для чтения с диска еще нету (шпиндель раскручивается). И вот уже при попытке чтения файла прочитается 0 байт.
import asyncio
import selectors
import os

devnull = os.open("/dev/null", os.O_RDONLY)

def reader():
    data = os.read(devnull, 50)
    print("Successfully read {} bytes from /dev/null".format(len(data)))

loop = asyncio.SelectorEventLoop(selectors.SelectSelector())
loop.add_reader(devnull, reader, )
loop.run_forever()


Скрипт бесконечно печатает «Successfully read 0 bytes from /dev/null».
С epoll будет ошибка при вызове 'add_reader'.
/dev/null вообще неудачный пример просто. Вот на kqueue (FreeBSD) ваш скрипт выкидывает эксепшн с /dev/null на любом селекторе, с обычным файлом все будет норм:

import asyncio
import selectors
import os

path = "/etc/fstab"
f = os.open(path, os.O_RDONLY)

def reader():
    data = os.read(f, 50)
    print("Successfully read {} bytes from {}".format(len(data), path))
    print("data: {}".format(data))

loop = asyncio.SelectorEventLoop(selectors.KqueueSelector())
loop.add_reader(f, reader, )
loop.run_forever()

Вывод:
Successfully read 50 bytes from /etc/fstab
data: b'# Device\t\tMountpoint\tFStype\tOptions\t\tDump\tPass#\n/d'
Successfully read 50 bytes from /etc/fstab
data: b'ev/ada0p2\t\tnone\tswap\tsw\t\t0\t0\n/dev/ada1p2 '
Successfully read 50 bytes from /etc/fstab
data: b' none swap sw 0 0\nlinpr'
Successfully read 50 bytes from /etc/fstab
data: b'oc /compat/linux/proc linprocfs rw 0 0\nfdesc /dev/'
Successfully read 50 bytes from /etc/fstab
data: b'fd fdescfs rw 0 0\ntmpfs /compat/linux/dev/shm'
Successfully read 24 bytes from /etc/fstab
data: b'\ttmpfs\trw,mode=1777\t0\t0\n'


Искренне полагал, что и epoll поддерживает, оказывается таки нет. Но все равно вывод — однозначно сказать да или нет нельзя, зависит от ОС.
Надо смотреть бенчмарки для каждого подхода и конкретной задачи. Могут быть неожиданности
Код на asyncio более легко писать и дебажить по сравнению с кодам на потоках. Так как разработчик сам определяет места, где происходит переключение контекста. Плюс потоки потребляют больше памяти чем корутины(можно конечно взять зеленые потоки, но это еще более сложно дебажить, чем просто потоки). Если же так важна производительность(при том что не понятно почему на потоках должно получаться значительно быстрее чем на asyncio), то может лучше выбрать не Пайтон? Пайтон все же больше про скорость разработки и понятный код.

Не со всеми определениями автора я согласен.


В ​синхронных операциях задачи выполняются друг за другом.

В данном случае не совсем удачное слово "задачи". Задачи могут быть вполне себе асинхронными, например пул обработчиков http запосов, но каждый из них может работать синхронно, тоесть последовательно: обработал параметры запроса, подготовил данные, передал ответ.


В асинхронных задачи могут запускаться и завершаться независимо друг от друга.

Правильнее говорить: в программе прописаны критерии, когда стартует асинхронная задача, но не время. Асинхронная задача может вообще не вызываться, или вызываться очень часто и исполняться, как в синхронном, так и асинхронном режиме. Простой пример: программа выполняет какую — то долгоиграющую операцию, и способна реагировать на Ctrl+C, нажмет оператор на эти кнопки или нет, нажмет, но потом передумает, и позволит программе продолжаться, или прервет ее, это зависит от оператора, но не от программы.


Конкурентность подразумевает, что две задачи выполняются совместно

Конкурентность подразумевает, что 2 задачи совместно используют одни и те же данные. Способ исполнения самих задач здесь не столь важен.


Параллелизм по сути является формой конкурентности.

Конкурентный доступ к данным является следствием появления возможности для паралельного исполнения кода.


Но параллелизм зависит от оборудования. Например, если в CPU только одно ядро, то две задачи не могут выполняться параллельно.

Да, 95 форточки, со своей кооперативной многозадачностью устарели, но они были, и для своего времени весьма не плохо справлялись.


Python поддерживает потоки уже очень давно. Потоки позволяют выполнять операции конкурентно.

Ядерные потоки в питоне — это скорее повод для головной боли разрабов, поэтому их не особо и используют. Байткод питона не атомарен, сам питон использует потоки для своих нужд, на сколько я помню, и поэтому у питона есть GIL, чтобы разраб ненароком чего нибудь не уронил, в процессе работы. А так, да, потоки в питоне есть.


По поводу async/await и asyncio, это не что иное, как синтаксический сахар над реализацией кооперативной многозадачности на одном потоке исполнения. Если вы много читаете по сети, или с диска, да, вы можете это использовать, и получить профит по количеству обработанных задач, но, например, на вычислительных задачах выигрыша не будет.

Гринлеты как-то пропустить умудрились в обзорной статьею.
Я так понял, автор предпочитает asyncio гринлетам, и ещё использует только встроенные решения.
Для CPU зависимых задач интерпретатор делает проверку каждые N тиков и переключает потоки.


Насколько мне известно, с версии Python 3.2 используются не тики, а миллисекунды.
жаль, что автор статьи не упомянул Twisted
В статье же не рассматривались фреймворки и сторонние библиотеки.
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории