Как понимать event-driven, blocking и non-blocking I/O
Когда Node.js только появился, появилось множество статей о том как же он хорош в мире Highload (к слову и не только в этом мире). Как же он быстро умеет обрабатывать входящие запросы. И как правило всегда все доводы сводились к тому, что Node.js использует event-driven подход. Это запомнили все, но как выяснилось понимают не все. Давайте разбираться.
Пример из жизни
Blocking I/O
Представим, что мы пришли в сеть общепита, где продают фастфуд. И вот кассир нам уже машет и кричит “Свободная касса”. Мы подходим и начинаем выбирать какой бургер нам съесть. Выбрали, заказали и… Ждем. Ждем, пока наш заказ будет готов. Ждем как после этого кассир возьмет у нас деньги, выбьет чек и отдаст нам заказ. И все это время кассир тоже ждет. То есть, говоря техническим языком, он принял наш запрос, и пока что-то делается согласно нашему запросу, кассир заблокирован нами (blocking). А тут уже и народу поднабежало. И выстроилась целая очередь на обслуживание. А кассир один. И как следствие: чтобы увеличить количество обрабатываемых заказов - нужно увеличить количество кассиров. А это, как следствие, повышенная стоимость на обслуживание. Такая система становится дороже.
Non-blocking I/O и event-driven подход
А теперь представим, что мы пришли все туда же. Кассир принял наш заказ, принял деньги и выдал нам чек с неким номером, который мы должны отслеживать на мониторе. Дальше он передает заказ куда-то дальше, где заказ будет готовиться и собираться, и при этом он готов обслуживать следующего клиента. То есть, здесь налицо отсутствие блокировки кассира (non-blocking I/O). И вот наш заказ готов, на экране появляется его номер, это своего рода событие, которое говорит о том, что заказ готов к выдаче, кассир берет его и отдает нам. Это и называется event-driven подходом.
Технически
Под I/O подразумеваются операции ввода/вывода, такие как чтение данных из файловой системы, или, например, БД. Обычно есть синхронный и асинхронный методы, для работы с такими ресурсами. Синхронные операции являются блокирующими, потому как требуют какой-то промежуток времени для их завершения, а обрабатывающий их поток - ждет пока они завершат свою работу.
Классический режим работы веб-сервера использует thread-based подход, то есть четко как в нашем первом примере. На каждый запрос создается отдельный поток, который продолжает жить, пока не выполнится. Сколько будет запросов - столько потоков и будет создано. И это вроде бы не проблема, если запросов немного, а если это Highload-проект, где количество запросов может быть огромным, то проблема становится весомой.
Представим в виде псевдокода thread-base подход с blocking I/O:
1 | var user = getUserByName('vladimir'); |
Как видно из примера выше, наш код синхронный. То есть выполняется линейно: строчка, где мы отдаем пользователя в формате JSON гарантировано всегда наступит после того, как мы получим пользователя. И вот этот промежуток времени между этими операциями будет простоем. Мощный сервер будет простаивать и просто ждать, вместо того, чтобы заниматься чем-то полезным в это время.
А теперь посмотрим на event-driven подход:
1 | getUserByName('vladimir', function(user){ |
Как видно из этого примера, мы использовали асинхронный подход. Мы как бы сказали серверу: “Получи данные пользователя vladimir“. А потом добавили: “Когда получишь - отдай их”. Здесь нужно понять разницу в этих двух подходах. В итоге сервер как тот кассир, принял заказ и пока не будет события о том, что он получил данные пользователя, он может заниматься другими вещами, например обрабатывать другой запрос другого клиента.
Примеры вокруг
Я часто говорю о том, что все эти примеры окружают нас каждый день. Действительно, мы ведем себя всегда как event-driven механизм. Мы идем есть тогда, когда возникает событие о голоде. Да, пожалуй, это касается всех базовых инстинктов.
Говоря о блокировке предлагаю подумать о том, как мы готовим (варим суп). Если бы мы все делали строго последовательно (синхронно), то мы блокировали бы себя только одной операцией. И пока, например, вода не нагреется (хотя от нас этот процесс не зависит), мы не переходим к подготовке других ингредиентов. В общем и целом, было бы все дольше и крайне неэффективно.