Pull to refresh

Что такое «асинхронная событийная модель», и почему сейчас она «в моде»

Reading time 15 min
Views 54K
Сейчас в тематических интернетах модно слово «Node.js». В этой небольшой статье мы попробуем понять («на пальцах»), откуда всё это взялось, и чем такая архитектура отличается от привычной нам архитектуры с «синхронным» и «блокирующим» вводом/выводом в коде приложения (обычный сайт на PHP + MySQL), запущенного на сервере приложений, работающем по схеме «по потоку (или процессу) на запрос» (классический Apache Web Server).

О читабельности статьи


Эта статья, со времени её появления здесь, подверглась множеству правок (в том числе концептуальных) и дополнений, благодаря обратной связи от читателей, упомянутых в конце статьи. Если Вам здесь сложен какой-то кусок для понимания, опишите это в комментариях, и мы распишем его в статье более понятным языком.

О производительности


Современные высоконагруженные сайты типа twitter'а, вконтакта и facebook'а работают на связках вида PHP + Apache + NoSQL или Ruby on Rails + Unicorn + NoSQL, и ничуть не тормозят. Во-первых, они используют NoSQL вместо SQL. Во-вторых, они распределяют запросы («балансируют») по множеству одинаковых рабочих серверов (это называется «горизонтальным масштабированием»). В-третьих, они кешируют всё, что можно: страницы целиком, куски страниц, данные в формате Json для Ajax'овых запросов, и т.п… Кешированные данные являются «статикой», и отдаются сразу серверами наподобие NginX'а, минуя само приложение.

Я лично не знаю, станет ли сайт быстрее, если его переписать с Apache + PHP на Node.js. В тематических интернетах можно встретить как тех, кто считает системные потоки медленнее «асинхронной событийной модели», так и тех, кто отстаивает противоположную точку зрения.

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

Например, если ваша программа поддерживает множество одновременных подключений, и постоянно пишет в них, и считывает из них, то в таком случае вам определённо следует посмотреть в сторону «асинхронной событийной модели» (например, в сторону Node.js'а). Node.js отлично подойдёт, если вы хотите перевести какую-нибудь подсистему на протокол WebSocket.

Примеры систем, которым хорошо подойдёт «асинхронная событийная модель»:
  • система в диспетчерской такси, следящая за перемещением каждого автомобиля, распределяющая поток пассажиров, высчитывающая оптимальные пути и т.п..
  • система поддержания жизнедеятельности, постоянно собирающая данные с множества раскиданных датчиков, и управляющая химическим составом, температурой, влажностью и т.п.
  • организм человека (мозг — логика управления, нервная система — канал передачи данных)
  • чат
  • MMORPG

Что такое «блокирующий» и «неблокирующий» вводы/выводы


Разберёмся с видами ввода/вывода на примере сетевого сокета («socket» – дословно «место соединения»), через который пользователь интернета соединился с нашим сайтом, и загружает на него картинку для аватара. В этой статье мы будем сравнивать «асинхронную событийную модель» с «привычной» архитектурой, где весь ввод/вывод в коде приложения — «синхронный» и «блокирующий». «Привычной» — просто потому что раньше всякими «блокировками» никто не заморачивался, и все так писали, и всем хватало. Что такое «синхронный» и «блокирующий» ввод/вывод? Это самый простой и обычный ввод/вывод, на котором пишется большая часть сайтов:
  • открыть файл
  • начать его считывать
  • ждать, пока не считается
  • файл считался
  • закрыть файл
  • вывести считанное содержимое на экран

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

При этом в коде нашей программы возникает «блокировка», во время которой поток простаивает, хотя мог бы заняться чем-нибудь полезным. Для решения этой задачи был придуман «синхронный» и «неблокирующий» ввод/вывод:
  • начать слушать сокет
  • если на нём нет новых данных, перестать слушать сокет
  • если на него уже поступила какая-нибудь порция данных картинки — считать эти данные
  • перестать слушать сокет

Если эти шаги выполнять в цикле, пока не будет считана последняя порция данных картинки, то мы опять же в итоге получим всю картинку полностью. С тем лишь отличием, что в этом цикле, помимо считывания данных с сокета, мы можем делать что-нибудь ещё полезное, а не простаивать под «блокировкой». Например, можно было бы считывать данные ещё и с другого сокета. Такой цикл «неблокирующего» ввода/вывода всплывёт ещё раз ближе к середине статьи.

Существует ещё «асинхронный» ввод/вывод. В нашей статье мы не будем его рассматривать, но вообще это когда мы вешаем на сокет «функцию обратного вызова» (callback) из нашего кода, которая будет вызываться операционной системой каждый раз, когда на этот сокет будет приходить очередная порция данных картинки. И дальше уже забываем о прослушивании этого сокета вообще, отправляясь делать другие дела. «Асинхронный» ввод/вывод, как и «синхронный», делится на «блокирующий» и «неблокирующий». Но в этой статье под словами «блокирующий» и «неблокирующий» мы будем иметь ввиду именно «синхронный» ввод / вывод.

И ещё, в этой статье мы будем рассматривать только «привычную» архитектуру, где приложение запущено непосредственно на операционной системе, с её системными потоками, а не на какой-нибудь «виртуальной машине» с её «зелёными потоками». Потому что внутри «виртуальной машины» с «зелёными потоками» можно творить разные чудеса, типа превращения якобы «синхронного» ввода/вывода в «асинхронный», о чём пойдёт речь ближе к концу статьи, в разделе «Альтернативный путь».

Предпосылки


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

Проверенная годами связка PHP + MySQL + Apache хорошо справлялась с «интернетом 1.0». Сервер запускал новый поток (или процесс, что почти одно и то же с точки зрения операционной системы) на каждый запрос пользователя. Этот поток шёл в PHP, оттуда – в базу данных, чего-нибудь там выбирал, и возвращался с ответом, который отсылал пользователю по HTTP, после чего самоуничтожался.

Однако, для приложений «реального времени» её стало не хватать. Допустим, у нас есть задача «поддерживать одновременно 10 000 соединений с пользователями». Можно было бы для этого создать 10 000 потоков. Как они будут уживаться друг с другом? Их будет уживать друг с другом системный «планировщик», задачей которого является выдавать каждому потоку его долю процессорного времени, и при этом никого не обделять. Действует он так. Когда один поток немного поработал, запускается планировщик, временно останавливает этот поток, и «подготавливает площадку» для запуска следующего потока (который уже ждёт в очереди).

Такая «подготовка площадки» называется «переключением контекста», и в неё входит сохранение «контекста» приостанавливаемого потока, и восстановление контекста потока, который будет запущен следующим. В «контекст» входят регистры процессора и данные о процессе в самой операционной системе (id’шники, права доступа, ресурсы и блокировки, выделенная память и т.д.).

Как часто запускается планировщик – это решает операционная система. Например, в Linux’е по умолчанию планировщик запускается где-то раз в сотую долю секунды. Планировщик также вызывается, когда процесс «блокируется» вручную (например, функцией sleep) или в ожидании «синхронного» и «блокирующего» (то есть, самого простого и обычного) ввода/вывода (например, запрос пользователя в потоке PHP ждёт, пока база данных выдаст ему отчёт по продажам за месяц).

В общем случае полагают, что «переключение контекста» между системными потоками не является таким уж дорогостоящим, и составляет порядка микросекунды.

Если потоки активно читают разные области оперативной памяти (и пишут в разные области оперативной памяти), то, при росте числа таких потоков, им станет не хватать «кеша второго уровня» (L2) процессора, составляющего порядка мегабайта. В этом случае им придётся каждый раз ожидать доставки данных по системной шине из оперативной памяти в процессор, и записи данных по системной шине из процессора в оперативную память. Такой доступ к оперативной памяти на порядки медленнее доступа к кешу процессора: для этого и был придуман этот кеш. В этих случаях, время «переключения контекста» может доходить до 50 микросекунд.

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

Рассмотрим ещё, какой отпечаток накладывает многопоточная модель на потребление приложением оперативной памяти. С каждым системным потоком связан «стек». Если поток вызывает некую функцию с аргументами, то в «стек» кладутся аргументы этой функции, и текущий адрес в коде, называемый «адресом возврата» (потому что по нему мы вернёмся сюда обратно, когда вызванная функция закончит выполняться). Если эта функция вызывает ещё какую-то функцию внутри себя, то соответствующие данные опять пишутся в «стек», поверх тех, которые уже были туда записаны, создавая таким образом подобие клубка.

При создании системного потока, «стек» выделяется операционной системой в оперативной памяти не сразу целиком, а «кусочками», по мере его использования. Это называется «виртуальной памятью». То есть, каждому потоку выделяется сразу большой кусок «виртуальной памяти» под «стек», но на деле вся эта «виртуальная память» дробится на «кусочки», называемые «страницами памяти», и уже эти «страницы памяти» выделяются в «настоящей» оперативной памяти только тогда, когда в них возникает необходимость. Когда поток дотрагивается до «страницы памяти», ещё не выделенной в «настоящей» оперативной памяти (например, пытается отдать процессору команду записать туда что-нибудь), «блок управления памятью» процессора засекает это действие, и вызывает в операционной системе «исключение» «page fault», на которое она отвечает выделением данной «страницы памяти» в «настоящей» оперативной памяти.

В Linux'е размер стека по-умолчанию равен 8-ми мегабайтам, а размер «страницы памяти» — 4-рём килобайтам (под «стек» сразу выделяются одна-две «страницы памяти»). В пересчёте на 10 000 одновременно запущенных потоков мы получим требование около 80 мегабайтов «настоящей» оперативной памяти. Вроде как немного, и вроде как нет повода для беспокойства. Но размер требуемой памяти в этом случае растёт как O(n), что говорит о том, что с дальнейшим ростом нагрузки могут возникнуть сложности с «масштабируемостью»: что, если завтра ваш сайт будет обслуживать уже 100 000 одновременных пользователей, и потребует поддержания 100 000 одновременных соединений? А послезавтра — 1 000 000? А после-послезавтра — ещё неизвестно сколько…

Однонитевые серверы приложений лишены такого недостатка, и не требуют новой памяти с ростом количества одновременных подключений (это называется O(1)). Взгляните на этот график, сравнивающий потребление оперативной памяти Apache Web Server'ом и NginX'ом:

image

Современные web-серверы (включая современный Apache) построены не совсем на архитектуре «по потоку на запрос», а на более оптимизированной: имеется «пул» заранее заготовленных потоков, которые обслуживают все запросы по мере их поступления. Это можно сравнить с аттракционом, в котором имеется 10 лошадей, и 100 ездоков, которые хотят прокатиться: образуется очередь, и пока первые 10 ездоков не прокатятся «туда и обратно», следующие 10 ездоков будут стоять и ждать в очереди. В данном случае аттракцион — это сервер приложений, лошади — потоки из пула, а ездоки — пользователи сайта.

Если мы будем использовать такой «пул» системных потоков, то одновременно мы сможем обслуживать только то количество пользователей, сколько потоков у нас будет «в пуле», то есть никак не 10 000.

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

Асинхронная событийная модель


Нужна была новая архитектура для подобного класса приложений. И в такой ситуации, как нельзя кстати, подошла «асинхронная событийная модель». В основе её лежат «событийный цикл» и шаблон «reactor» (от слова «react» – реагировать).

«Событийный цикл» представляет собой бесконечный цикл, который опрашивает «источники событий» (дескрипторы) на предмет появления в них какого-нибудь «события». Опрос происходит с помощью библиотеки «синхронного» ввода/вывода, который, при этом будет являться «неблокирующим» (в системную функцию ввода/вывода передаётся флаг O_NONBLOCK).

То есть, во время очередного витка «событийного цикла», наша система проходит последовательно по всем дескрипторам, и пытается считать из них «события»: если таковые имеются, то они возвращаются функцией чтения в нашу систему; если же никаких новых событий у дескриптора нет, то он не станет «блокировать» и ждать появления «события», а сразу же возвратит ответ: «новых событий нет».

«Событием» может быть приход очередной порции данных на сетевой сокет («socket» – дословно «место соединения»), или считывание новой порции данных с жёсткого диска: в общем, любой ввод/вывод. Например, когда вы загружаете картинку на хостинг, данные туда приходят кусками, каждый раз вызывая событие «новая порция данных картинки получена».

«Источником событий» в данном случае будет являться «дескриптор» (указатель на поток данных) того самого TCP-сокета, через который вы соединились с сайтом по сети.

Второй компонент новой архитектуры, как уже было сказано, – это шаблон «reactor». И, для русского человека, это совсем не тот реактор, который стоит на атомной станции. Суть этого шаблона заключается в том, что код сервера пишется не одним большим куском, который исполняется последовательно, а небольшими блоками, каждый из которых вызывается («реагирует») тогда, когда происходит связанное с ним событие. Таким образом, код представляет собой набор множества блоков, задача которых состоит в том, чтобы «реагировать» на какие-то события.

Такая новая архитектура стала «мейнстримом» после появления Node.js’а. Node.js написан на C++, и основывает свой событийный цикл на Сишной библиотеке «libev». Однако Яваскрипт здесь не является каким-то избранным языком: при наличии у языка библиотеки «неблокирующего» ввода/вывода, для него тоже можно написать подобные «фреймворки»: у Питона есть Twisted и Tornado, у Перла – Perl Object Environment, у Руби – EventMachine (которой уже лет пять). На этих «фреймворках» можно писать свои серверы, подобные Node.js’у. Например, для Явы (на основе java.nio) написаны Netty и MINA, а для Руби (на основе EventMachine) – Goliath (который ещё и пользуется преимуществами Fibers).

Преимущества и недостатки


«Асинхронная событийная модель» хорошо подойдёт там, где много-много пользователей одновременно производят какие-нибудь действия, не нагружающие процессор. Например: получают температуру с датчиков в режиме «текущего времени», получают изображения с видеокамер, передают на сервер температуру, снятую с прикреплённых к ним градусников, пишут новые сообщения в чат, получают новые сообщения из чата, и т.п…

Требование действий, не нагружающих процессор, проясняется, когда мы вспоминаем о том, что весь этот бесконечный цикл запущен в одном единственном потоке, и если вы вклините в этот цикл какое-нибудь тяжеловесное вычисление (скажем, начнёте решать дифференциальное уравнение), то все остальные пользователи будут ждать в очереди, пока одно это вычисление не закончится.

Поэтому серверы, подобные Node.js’у, подходит только для выполнения задач, не нагружающих процессор, или как «фронтенд» для тяжеловесного «бекенда». А также они подходят как серверы по обслуживанию «медленных» запросов (узкий канал связи, медленная отдача/посылка данных, долгое время отклика где-то внутри, ...). Я бы отвёл серверам, подобным Node.js’у, место «вводяще-выводящего» посредника. Например, место посредника между «клиентом» и «сервером»: всё зрительное представление создаётся и отрисовывается непосредственно в обозревателе пользователя интернета, все нужные данные хранятся на сервере в хранилище, а Node.js выполняет задачу посредника, выдавая «клиенту» требуемые данные по запросу, и записывая в хранилище новые данные, когда они приходят от «клиента».

То обстоятельство, что серверы по «асинхронной событийной модели» запущены в одном системном потоке, на практике порождает ещё два препятствия. Первое — утечки памяти. Если Apache создаёт по системному потоку на каждый новый запрос, то, после отправки ответа пользователю, этот системный поток самоуничтожается, и вся выделенная ему память просто высвобождается. В случае же со, скажем, Node.js'ом, разработчику следует быть осторожным, и не оставлять следов при обработке очередного запроса пользователя (унижчтожать из оперативной памяти все улики того, что такой запрос вообще приходил), иначе процесс будет пожирать больше и больше памяти с каждым новым запросом. Второе — это обработка ошибок программы. Если, опять же, обычный Apache создаст отдельный системный поток для обработки входящего запроса, и обрабатывающий код на PHP выбросит какое-нибудь «исключение», то этот системный поток просто тихо «умрёт», а пользователь получит в ответ страницу типа «500. Internal Server Error». В случае же того же Node.js'а, единственная ошибка, возникшая при обработке единственного запроса, «положит» весь сервер целиком, из-за чего его придётся мониторить и перезапускать вручную.

Ещё один возможный недостаток «асинхронной событийной модели» – иногда (не всегда, но бывает, особенно при использовании «асинхронной событийной модели» для того, для чего она не предназначена) код приложения может стать сложным для понимания из-за переплетения «обратных вызовов». Это называется проблемой «спагетти-кода», и описывается так: «коллбек на коллбеке, коллбеком погоняет». С этим пытаются бороться, и, например, для Node.js’а написана библиотека Seq.

Ещё один путь устранения «обратных вызовов» вообще — так называемые continuations (coroutines). Они введены, например, в Scala, начиная с версии 2.8 (coroutines), и в Руби, начиная с версии 1.9 (Fibers). Вот пример того, как помощью Fibers в Руби можно полностью устранить коллбеки, и писать код так, как будто бы всё происходит синхронно.

Для Node.js'а была написана аналогичная библиотека node-fibers. По производительности (в искусственных тестах, не в реальном приложении) node-fibers пока работают где-то в три-четыре раза медленнее обычного стиля с «обратными вызовами». Автор библиотеки утвреждает, что эта разница в производительности возникает там, где Яваскрипт стыкуется с C++'ным кодом движка V8 (на котором основан сам Node.js), и что замеры производительности нужно трактовать не как «node-fibers в три-четыре раза медленнее коллбеков», а как «по сравнению с остальными низкоуровневыми действиями в вашем коде (работа с байтовыми массивами, подключение к базе данных или к сервису в интернете), отпечаток производительности node-fibers совсем не будет заметен».

В дополнение к привычному стилю программирования, node-fibers возвращает нам ещё и привычный и удобный способ обработки ошибок try/catch'ами. Однако эта библиотека не будет внедрена в ядро Node.js'а, поскольку Райан Даль видит предназначение своего творения в том, чтобы оставаться низкоуровневым и не скрывать ничего от разработчика.

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

Альтернативный путь


В этой статье мы объяснили, почему приложение, использующее «синхронный» и «блокирующий» ввод/вывод, не выдерживает большого количества одновременных подключений. В качестве одного из решений мы предложили перевод этого приложения на «асинхронную событийную модель» (то есть, переписать приложение, скажем, на Node.js'е). Этим способом мы решим задачу фактически (закулисным) переходом с «синхронного» и «блокирующего» ввода/вывода на «синхронный» и «неблокирующий» ввод/вывод. Но это не единственное решение: мы также можем прибегнуть к «асинхронному» вводу/выводу.

А именно, мы можем использовать старый добрый «пул» системных потоков (описанный ранее в этой статье), эволюционировавший на новую ступень развития. Эта ступень развития называется «зелёные процессы» (соответственно, есть ещё и «зелёные потоки»). Это процессы, но не системные, а созданные виртуальной машиной того языка, на котором написан наш код. Виртуальная машина запускает внутри себя обычный «пул» системных потоков (скажем, по количеству ядер в процессоре), и уже на эти системные потоки отображает свои внутренние «зелёные процессы» (полностью скрывая это от разработчика).

«Зелёные процессы» — это именно «процессы», а не «потоки», так как они не имеют никаких общих переменных друг с другом, а общаются только посылкой управляющих «сообщений» друг другу. Такая модель обеспечивает защиту от разных «deadlock»'ов и избегает проблем с совместным доступом к данным, ибо всё, что имеет «зелёный процесс» — это его внутреннее состояние и «сообщение».

Каждый «объект» имеет свою очередь «сообщений» (для этого создаётся «зелёный процесс»). И любой вызов кода «объекта» — это посылка «сообщения» ему. Посылка «сообщений» от одного «объекта» другому «объекту» происходит асинхронно.

В дополнение к этому, виртуальная машина создаёт свою подсистему ввода/вывода, которая отображается на неблокирующий системный ввод/вывод (и снова разработчик ни о чём не подозревает).

И, конечно же, виртуальная машина ещё содержит свой внутренний планировщик.

В итоге, разработчик думает, что он пишет обычный код, с обычным вводом/выводом, а на деле выходит очень высокопроизводительная система. Примеры: Erlang, Actor'ы в Scala.

Как «событийный цикл» опрашивает «источники событий» на предмет появления в них новых данных


Самое простое решение, которое можно придумать – опрашивать все «дескрипторы» (открытые сетевые сокеты, считываемые или записываемые файлы, …) на предмет наличия в них новых данных. Такой алгоритм называется «poll». Выглядит это примерно так:
  • у вас есть два открытых сокета
  • вы создаёте массив из двух структур, которые описывают эти сокеты
  • каждому элементу этого массива вы проставляете, что и о каком сокете в него записать
  • затем вы передаёте этот массив системной функции poll, которая пишет туда описание текущего состояния этих сокетов
  • после этого вы проходитесь по этому массиву ещё раз, выясняя, есть ли там для этих сокетов новые данные
  • если есть – считываете их, и делаете с ними что-нибудь
  • всё это повторяется для нового витка бесконечного «событийного цикла»
Причём упомянутый массив с данными передаётся не по ссылке, а именно копируется, по причине того, что операционная система состоит из «пространства ядра» и «пользовательского пространства», которые не могут иметь общих кусков памяти (для обеспечения безопасности системы).

Поскольку доставка данных в регистры процессора из оперативной памяти, и отсылка данных из регистров процессора в оперативную память, не являются быстрыми операциями, то такое копирование массива туда-сюда сказывается на производительности системы (процессор простаивает, пока данные по системной шине уходят в оперативную память и приходят из неё).

При этом большинство (около 95%) полученного массива (для порядка 10 000 открытых сокетов) являются бесполезными, так как соответствующие сокеты не имеют новых данных.

А раз размер этого массива растёт пропорционально количеству дескрипторов, то получается, что алгоритм этот работает тем медленнее, чем больше сокетов открыто. То есть, чем больше одновременных посетителей на вашем сайте, тем больше «событийный цикл» начинает тормозить. В таком случае говорят: «алгоритм имеет сложность O(n)».

Можно ли написать более оптимальный алгоритм? Можно, и такие были написаны в основных серверных операционных системах: epoll в Linux’е и kqueue во FreeBSD. В Windows'е также имеется IO Completion Ports, которая является своего рода близким родственником epoll'а, и была использована разработчиками Node.js'а при переносе его на Windows, для чего ими была написана библиотека libuv, предоставляющая единый интерфейс как для libev, так и для IO Completion Ports.

Рассмотрим epoll. Он отличается от простого poll’а с двух сторон.
  • Программа не проверяет вообще все дескрипторы (и все виды событий), а подписывается только на те дескрипторы (и виды событий), которые ей нужны.
  • Ядро делает область памяти с данными дескрипторов видимой программе путём создания файла /dev/epoll (на самом деле это «устройство», но с точки зрения философии Linux'а «всё есть файл»). Этот файл программа может читать (и писать в него) с помощью функции mmap без какого-либо копирования вообще. Наличие новых сообщений при этом проверяется системной функцией ioctl

И если обычный перебор дескрипторов занимал O(n) времени, то эти оптимизированные алгоритмы требуют O(1) времени, то есть не становятся медленнее с ростом количества одновременных посетителей на сайте.

Пользователи, принявшие участие в правке статьи


Статья включает смысловые правки, предложенные пользователями: akzhan, erlyvideo, eyeofhell, MagaSoft, Mox, nuit, olegich, reddot, splav_asv, tanenn, Throwable.
А также синтаксические и стилистические правки, замеченные пользователями: Goder, @theelephant.

Ссылки по теме


Вы наверное шутите, мистер Дал, или почему Node.js — это венец эволюции веб-серверов
Введение в EventMachine
Scalable network programming
Kqueue
Stackless Python и Concurrence
Чем отличаются блокирующий и неблокирующий вводы/выводы (а также асинхронный, и другие)
node-sync — псевдо-синхронное программирование на nodejs с использованием fibers
Как работает кеш процессора
What every programmer should know about memory
10 things every Linux programmer should know
Video: Node.js by Ryan Dahl
No Callbacks, No Threads & Ruby 1.9
Tags:
Hubs:
+142
Comments 130
Comments Comments 130

Articles