Корутинные будни, часть 2 из 3.
Вытесняющая и кооперативная многозадачность
Лет 30 уже все неспециализированные операционные системы — многозадачные. Что на сервере, что на телефоне, это платформы чтобы можно было запустить прорву разного софта, и он там крутился — грел воздух, разряжал батарею, иногда случайно делал что-нибудь полезное. А операционная система следит, кому какие данные пришли, и распихивает процессы по ядрам. Если кто-то засел за вычислениями, а новых данных не просит, его могут принудительно поставить на паузу, если вдруг кому другому надо повычисляться. Это называется вытесняющей многозадачностью: тебе дадут процессор, но если ты повис в своих циклах, или в блокирующем ожидании данных — тебя могут вытеснить и дать процессор другому.
Внутри, скажем, веб-сервера, дела обстоят немного иначе. Там одна программа (специально не говорю «один процесс», а то всякое бывает), но много источников данных. Это может быть уже существующий клиент, которые послал ещё байтик своего запроса, может быть новый клиент, желающий соединения, могут быть данные «с обратной стороны» — скажем, если мы хотим сгенерировать страничку, для этого послали запрос к базе данных, и вот получили ответ.
Как делают хорошие веб-серверы: хранят какую-нибудь структурку данных для клиента. На каждое приходящее событие понимают, к какому клиенту это относится, быстро что-нибудь меняют в соответствующей структуре, ждут следующего события (которое может иметь отношение к другому клиенту, ну и что). Несколько процессов/потоков для таких порой имеет смысл, но не определяющий. Это называется кооперативной многозадачностью: внутри сервера обрабатывается много клиентов, но они не спорят за вычислительные ресурсы, а отдают друг другу место, когда заканчивают свои дела. Зато если обработка какого-то клиента внезапно займёт много времени, ждать придётся и остальным тоже.
Костыли и принцип чайника
Как делают плохие серверы: на каждого пришедшего клиента просят операционную систему создать поток/процесс, и в этом процессе работают только с тем клиентом. Почему это плохо?
Это очень сложная архитектура. На любую мелочь (начиная со статистики — хотя бы счётчика запросов), которая нужна всем клиентам, приходится городить синхронизирующие примитивы. Добро пожаловать в мир мьютексов, блокировок, атомарных операций, барьеров памяти и просранных кеш-линий.
Это очень неэффективно. Каждый поток требует структуры данных для его состояния, записи в недрах ядра, выделенный стек, а переключение между потоками занимает много времени и снова куда-то делся весь кеш.
Почему же тогда много софта (особенно старого — mysql плодит по потоку на клиента, apache по процессу) всё ещё сидит на этом кактусе? Потому что многопроцессорность/многопоточность — уже существующие решения, и их сложности — привычные сложности для старшего поколения. Даже если асинхронный подход проще и эффективнее, в нём всё равно надо разбираться.
Асинх-что?
Если оставить в стороне экзотику вроде функционального программирования (у них там и так всё хорошо: продолжения, колбеки на колбеках, вот это всё), мы привыкли писать синхронный императивный код: получить данные, обработать данные, отдать данные, вот это всё. Если в серединке надо «получить ещё данных», то жизнь сразу становится чуть сложнее.
Сложность ровно в том, что это «получить ещё данных» на самом деле — ждать, пока придут данные, если пришли — продолжить, а если пришли чьи-то другие — продолжить другую задачу, а потом ждать дальше. Такие фокусы один маленький recv () делать не умеет, придётся изобретать что-то похитрее.
В простых случаях можно отделить сбор данных от вычислений: скажем, байтики из сети собирать в буфер, а когда там накопится запрос целиком — уже обработать как полагается. Так, например, работают (по крайней мере, работали когда-то) кошачьи движки (KittenDB), на которых крутится VK и Telegram. В более общем случае, когда мы хотим данных, надо как-то разорвать исполнение на «до» и «после».
Вытесняющая и кооперативная многозадачность
Лет 30 уже все неспециализированные операционные системы — многозадачные. Что на сервере, что на телефоне, это платформы чтобы можно было запустить прорву разного софта, и он там крутился — грел воздух, разряжал батарею, иногда случайно делал что-нибудь полезное. А операционная система следит, кому какие данные пришли, и распихивает процессы по ядрам. Если кто-то засел за вычислениями, а новых данных не просит, его могут принудительно поставить на паузу, если вдруг кому другому надо повычисляться. Это называется вытесняющей многозадачностью: тебе дадут процессор, но если ты повис в своих циклах, или в блокирующем ожидании данных — тебя могут вытеснить и дать процессор другому.
Внутри, скажем, веб-сервера, дела обстоят немного иначе. Там одна программа (специально не говорю «один процесс», а то всякое бывает), но много источников данных. Это может быть уже существующий клиент, которые послал ещё байтик своего запроса, может быть новый клиент, желающий соединения, могут быть данные «с обратной стороны» — скажем, если мы хотим сгенерировать страничку, для этого послали запрос к базе данных, и вот получили ответ.
Как делают хорошие веб-серверы: хранят какую-нибудь структурку данных для клиента. На каждое приходящее событие понимают, к какому клиенту это относится, быстро что-нибудь меняют в соответствующей структуре, ждут следующего события (которое может иметь отношение к другому клиенту, ну и что). Несколько процессов/потоков для таких порой имеет смысл, но не определяющий. Это называется кооперативной многозадачностью: внутри сервера обрабатывается много клиентов, но они не спорят за вычислительные ресурсы, а отдают друг другу место, когда заканчивают свои дела. Зато если обработка какого-то клиента внезапно займёт много времени, ждать придётся и остальным тоже.
Костыли и принцип чайника
Как делают плохие серверы: на каждого пришедшего клиента просят операционную систему создать поток/процесс, и в этом процессе работают только с тем клиентом. Почему это плохо?
Это очень сложная архитектура. На любую мелочь (начиная со статистики — хотя бы счётчика запросов), которая нужна всем клиентам, приходится городить синхронизирующие примитивы. Добро пожаловать в мир мьютексов, блокировок, атомарных операций, барьеров памяти и просранных кеш-линий.
Это очень неэффективно. Каждый поток требует структуры данных для его состояния, записи в недрах ядра, выделенный стек, а переключение между потоками занимает много времени и снова куда-то делся весь кеш.
Почему же тогда много софта (особенно старого — mysql плодит по потоку на клиента, apache по процессу) всё ещё сидит на этом кактусе? Потому что многопроцессорность/многопоточность — уже существующие решения, и их сложности — привычные сложности для старшего поколения. Даже если асинхронный подход проще и эффективнее, в нём всё равно надо разбираться.
Асинх-что?
Если оставить в стороне экзотику вроде функционального программирования (у них там и так всё хорошо: продолжения, колбеки на колбеках, вот это всё), мы привыкли писать синхронный императивный код: получить данные, обработать данные, отдать данные, вот это всё. Если в серединке надо «получить ещё данных», то жизнь сразу становится чуть сложнее.
Сложность ровно в том, что это «получить ещё данных» на самом деле — ждать, пока придут данные, если пришли — продолжить, а если пришли чьи-то другие — продолжить другую задачу, а потом ждать дальше. Такие фокусы один маленький recv () делать не умеет, придётся изобретать что-то похитрее.
В простых случаях можно отделить сбор данных от вычислений: скажем, байтики из сети собирать в буфер, а когда там накопится запрос целиком — уже обработать как полагается. Так, например, работают (по крайней мере, работали когда-то) кошачьи движки (KittenDB), на которых крутится VK и Telegram. В более общем случае, когда мы хотим данных, надо как-то разорвать исполнение на «до» и «после».