Мой выбор был бы выбран

Вы можете прочитать эту статью в моем блоге, если вы предпочитаете средний уровень.

Select, poll и epoll - это системные вызовы Linux, которые выполняют аналогичную задачу, они обеспечивают эффективный способ выполнения асинхронного ввода-вывода.

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

В эпоху, когда многое из того, что мы делаем, осуществляется по сети, эти системные вызовы asyncio имеют жизненно важное значение для большинства программистов.

Тем не менее, немногие разработчики слышали о них. Еще меньшее количество людей фактически использовали их напрямую. Несмотря на это, практически каждый, кто использует Node, Ruby, Go или Python, полагается на них, даже не подозревая об этом.

Большинство из нас, вероятно, предпочитает большую часть времени избегать работы с системными вызовами напрямую. Однако используемые нами библиотечные оболочки обычно тривиальны, мы можем в любой момент вернуться к собственным интерфейсам, при условии, что нам нужно снизить производительность или гибкость.

Я готов поспорить, что почти все достойные разработчики будут достаточно уверены в своих знаниях потоков Linux, чтобы использовать pthread.h, не задумываясь. Я также готов поспорить, что почти 100% этих разработчиков испугались бы, если бы им пришлось быстро создать безошибочное приложение на основе epoll.

Во-первых, я думаю, нам следует взглянуть на еще одно существо в бестиарии параллелизма Linux - поток.

Давайте посмотрим на 3 примера использования потоков. Во-первых, используя стандартные библиотеки Python (язык «высокого уровня») и Rust (язык «низкого уровня»), а затем используя собственный интерфейс, который Linux предоставляет для своего первоклассного гражданина C.

Python

Примечание: хотя можно услышать, что потоки Python не являются «настоящими потоками» из-за GIL, они по-прежнему используют pthread в своей реализации. У них не было бы проблем с работой как «настоящих» потоков в реализации python, отличной от GIL.

Ржавчина

C (POSIX API)

Очевидно, что использование собственного API pthread немного неудобно, но оболочки над pthread по-прежнему очень похожи на его использование напрямую, они просто немного упрощают его. Если вы понимаете, как использовать потоки из стандартной библиотеки любого языка, вы поймете, как использовать собственный API.

Затем давайте посмотрим на подход «нижнего уровня» для асинхронного ввода-вывода в Python и Rust, а затем посмотрим, как мы будем это делать с помощью собственного API.

В этом примере мы рассмотрим одновременную связь с 2 разными адресами через TCP.

Python

Ржавчина

Здесь я должен упомянуть, что немного схитрил. Поскольку futures и tokio не входят в стандартную библиотеку Rust. Однако у ржавчины еще нет стандарта asyncio, и обсуждение его реализации в основном ведется вокруг интеграции tokio & Futures в стандартную библиотеку. Таким образом, высока вероятность того, что окончательная версия клиента asyncio TCP в ржавчине будет очень похожа на приведенный выше код.

C (POSIX API)

И здесь возникает проблема.

Я хотел бы показать вам идиоматический способ использования poll, epoll или select для синхронного выполнения двух запросов через TCP, но я, вероятно, ошибаюсь. Более того, код будет длинным, и его будет очень трудно пролистать.

Вот очень хорошо продуманный TCP-клиент, использующий опрос. Заметили проблему? Он охватывает 700 строк кода.

Конечно, частично это вина C и ошибка встроенных сетевых функций. Но написать простой TCP-клиент не так уж сложно, это та часть, в которую мы должны включить asyncio, что делает невозможным работу правильно.

Я бы бросил вызов любому читателю, который думает иначе, найти мне идиоматический пример использования опроса, который даже близок по простоте подходов Rust и Python.

Как исправить асинхронные системные вызовы

Если приведенный выше код C был для вас слишком трудным для чтения, позвольте мне нарушить использование асинхронных системных вызовов для вас несколькими небольшими фрагментами кода:

  • Сделайте файловый дескриптор неблокирующим, как здесь.
  • Прокрутите неблокирующие файловые дескрипторы и посмотрите, произошли ли какие-либо события на каком-либо из них. Как здесь видно, но только с одним fd.
  • Если событие и произошло, обработайте его, а затем перенесите цикл. Если ничего не произошло, продолжайте.

Это не кажется * таким * плохим интерфейсом, за исключением того факта, что понимание того, как писать код на основе poll / epoll / select, и его написание - это две совершенно разные концепции.

Даже если предположить, что интерфейс был проще, он все равно совершенно чужд для любого, кто использовал любую другую библиотеку asyncio. По сути, вы начинаете с нуля, если хотите использовать собственный API.

Если бы был более простой способ…

Что ж, есть один способ, которым буквально все другие библиотеки обрабатывают asyncio, используя обратные вызовы и / или фьючерсы. Так делают все остальные языки с встроенной поддержкой asyncio, такие как Go, Python и Node. Так делают все библиотеки asyncio, будь то библиотеки для C, C ++, Java, Scala, Ruby, Perl, Php, Rust или любого другого языка под солнцем.

Но разве для Linux кошерно скрывать всю эту сложность под капотом? Иметь удобный API, который могут использовать грязные крестьяне, не понимающие истинной эстетики программирования.

На что я отвечаю, взгляните на pthread. Таким же образом мы используем pthread_create вместо системного вызова clone. Мы могли бы иметь удобную оболочку над асинхронными кодами без потери какой-либо функциональности с помощью волшебных указателей на функции.

Конечно, могут быть крайние случаи, когда нужно использовать исходные системные вызовы, аналогично тому, как есть крайние случаи, когда вам может потребоваться использовать clone вместо fork или pthread.

Но в 99,9% случаев я думаю, что мы были бы более чем довольны оберткой в ​​духе:

poll_events(underlying_syscall, fds_array, callback)

Если функции обратного вызова будут стандартизированы для получения дескриптора файла и любых дополнительных аргументов, специфичных для базового системного вызова. Например, два короткометражных фильма (ивенты и ревенты) для poll.

Или, может быть, этот интерфейс может даже предоставить стандартизованный способ работы одной и той же функции с любым из трех системных вызовов.

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

Если люди не могут использовать ваш API, возможно, это признак того, что ваш API вздор, а не признак того, что ваши пользователи глупы. Если системные вызовы «хлеб и масло» понятны и используются только избранными разработчиками библиотеки, это означает, что вам нужен лучший API.

С момента появления select появилось две возможности улучшить интерфейс. Один, когда был введен опрос, другой, когда был представлен epoll. Тем не менее, мы застряли с 3 громоздкими механизмами asyncio с немного разными API и реализациями, и все они одинаково непригодны для использования.

Если вам понравилась эта статья, вам также могут понравиться: