Как понимать 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
2
3
var user = getUserByName('vladimir');
//waiting ...
response.json(user);

Как видно из примера выше, наш код синхронный. То есть выполняется линейно: строчка, где мы отдаем пользователя в формате JSON гарантировано всегда наступит после того, как мы получим пользователя. И вот этот промежуток времени между этими операциями будет простоем. Мощный сервер будет простаивать и просто ждать, вместо того, чтобы заниматься чем-то полезным в это время.

А теперь посмотрим на event-driven подход:

1
2
3
getUserByName('vladimir', function(user){
response.json(user);
});

Как видно из этого примера, мы использовали асинхронный подход. Мы как бы сказали серверу: “Получи данные пользователя vladimir“. А потом добавили: “Когда получишь - отдай их”. Здесь нужно понять разницу в этих двух подходах. В итоге сервер как тот кассир, принял заказ и пока не будет события о том, что он получил данные пользователя, он может заниматься другими вещами, например обрабатывать другой запрос другого клиента.

Примеры вокруг

Я часто говорю о том, что все эти примеры окружают нас каждый день. Действительно, мы ведем себя всегда как event-driven механизм. Мы идем есть тогда, когда возникает событие о голоде. Да, пожалуй, это касается всех базовых инстинктов.

Говоря о блокировке предлагаю подумать о том, как мы готовим (варим суп). Если бы мы все делали строго последовательно (синхронно), то мы блокировали бы себя только одной операцией. И пока, например, вода не нагреется (хотя от нас этот процесс не зависит), мы не переходим к подготовке других ингредиентов. В общем и целом, было бы все дольше и крайне неэффективно.