Статьи

Node.js для программистов PHP # 1: программирование на основе событий … и Pasta.

Для разработчика PHP асинхронность является самым загадочным аспектом среды выполнения Node.js. Это просто новый способ написания программ. И как только вы пройдете первые этапы обучения, программирование на основе событий открывает мир возможностей, о которых программисты PHP никогда не мечтали бы. Я постараюсь объяснить вам, как это работает, но сначала давайте поговорим о пасте.

Spaghetti_with_tomato_sauce

Простой рецепт пасты

Я люблю хорошие макароны. Но готовить плохую пасту очень легко, поэтому у меня есть девиз: не ешьте макароны, приготовленные людьми, которым вы не доверяете. Поскольку вы — человек, которому я доверяю, я дам вам простой рецепт, который вы не можете испортить, чтобы, когда мы встречаемся в реальной жизни, мы могли есть вместе.

Рецепт — спагетти со свежим томатным соусом. Положите в кастрюлю большое количество воды вместе с щепоткой соли — до кипения потребуется около 10 минут. А пока очистите кучу свежих помидоров и нарежьте их примерно. Также очистите и измельчите лук, морковь, немного чеснока и стебель сельдерея. Эта смесь называется Mirepoixили, как говорят итальянцы, софритто. В это время вода должна кипеть, поэтому вы можете положить спагетти в кастрюлю. Пока он кипит, положите немного оливкового масла во вторую кастрюлю и нагрейте сухарик в течение нескольких минут. Затем добавьте помидоры, немного травы, соль и перец и перемешайте. Вы должны нагревать помидоры максимум пять минут, потому что после этого они создают определенную кислотность, которая исчезает только после часа нагревания. Попробуйте спагетти на регулярной основе, чтобы быть уверенным, что вытащите их из воды, когда они готовятся в аль денте . Слейте его, подайте на теплой тарелке с соусом, добавьте немного шиповника с базиликом и немного сыра пармезан. Вуаля.

Синхронное программирование

Если после прочтения вы не голодаете, вы можете выполнить следующее упражнение: как попросить компьютер приготовить мои спагетти со свежим томатным соусом? В PHP это будет выглядеть следующим образом:

<?php
// time is 0
$pastaPan = new Pan();
$water = new Water();
$pastaPan->fill($water);
$pastaPan->warm($duration = 10);
// now time is 10
$pastaPan->fill(new Spaghetti());
$pastaPan->warm($duration = 8);
// now time is 18
$pastaPan->remove($water);

$saucePan = new Pan();
$saucePan->fill(new OliveOil());
$saucePan->warm($duration = 2);
// now time is 20
$saucePan->fill(MirepoixFactory::create($withGarlic = true));
$saucePan->warm($duration = 5);
// now time is 25
$saucePan->fill(TomatoFactory::create());
$saucepan->warm($duration = 4);
// now time is 29

$plate = new Plate();
$plate->addContentsOf($pastaPan);
$plate->addContentsOf($saucePan);
$plate->serve('Voilà');

С этой программой есть большая проблема: весь рецепт занимает 29 минут, а спагетти прибывает в блюдо уже холодным, потому что ждет более десяти минут, пока соус не будет готов. Холодные спагетти — это преступление, вы должны избегать его всеми средствами. Когда я готовлю эти спагетти, я кипятим воду одновременно с рублением овощей. Прочтите еще раз рецепт: он содержит такие признаки, как «в то же время», «пока он кипит» и «регулярно пробовать». Программа PHP не делает этого, неудивительно, что производимые ею спагетти несъедобны.

Асинхронное программирование

Давайте научим PHP правильно готовить. Для этой цели лучшим шаблоном проектирования является программирование на основе событий . Теперь программа опирается на центральный  EventLoopкласс, который представляет службу, которая зацикливается, увеличивая внутренний тик в каждом цикле. Другие сервисы могут добавлять обратные вызовы для выполнения в данный момент. Когда все добавленные обратные вызовы выполнены, цикл останавливается.

<?php
class EventLoop
{
  protected $tick = 0;
  protected $callbacksForTick = array();

  public function start()
  {
    while ($this->callbacksForTick) {
      $this->tick++;
      $this->executeCallbacks();
    }
  }

  public function executeCallbacks()
  {
    echo "Tick is " . $this->tick . "\n";
    if (!isset($this->callbacksForTick[$this->tick])) {
      return; // no callback to execute
    }
    foreach ($this->callbacksForTick[$this->tick] as $callback) {
      call_user_func($callback, $this);
    }
    // clean up
    unset($this->callbacksForTick[$this->tick]);
  }

  public function executeLater($delay, $callback)
  {
    $this->callbacksForTick[$this->tick + $delay] []= $callback;
  }
}

Используя преимущества этого цикла событий, можно создать новый класс обслуживания: асинхронные службы. Например, асинхронный панорамирование:

<?php
class AsynchronousPan extends Pan
{
  protected $eventLoop;

  public function __construct(EventLoop $eventLoop)
  {
    $this->eventLoop = $eventLoop;
  }

  public function warm($duration, $callback)
  {
    $this->eventLoop->executeLater($duration, $callback);
  }
}

Комбинация EventLoopи в AsynchronousPanосновном требует передачи обратного вызова warm(), и этот обратный вызов может быть простой анонимной функцией:

<?php
$eventLoop = new EventLoop();
$pan = new AsynchronousPan($eventLoop);
echo "Starting to warm\n";
$pan->warm(10, function() {
  echo "Now it's cooked\n";
});
$eventLoop->start();

Когда вы выполняете этот скрипт, он производит следующий вывод:

Starting to warm
Tick is 1
Tick is 2
Tick is 3
Tick is 4
Tick is 5
Tick is 6
Tick is 7
Tick is 8
Tick is 9
Tick is 10
Now it's cooked

Асинхронная кулинария

Теперь все готово для другого соуса для спагетти. На этот раз можно ли готовить соус и спагетти одновременно? Так как спагетти занимает 18 минут, а соус — 11 минут, программа должна подождать около 7 минут, чтобы начать готовить соус.

<?php
$eventLoop = new EventLoop();

$plate = new Plate();

$pastaPan = new AsynchronousPan($eventLoop);
$water = new Water();
$pastaPan->fill($water);
echo "pastaPan: Starting to boil water\n";
$pastaPan->warm($duration = 10, function() use ($pastaPan, $plate, $water) {
  echo "pastaPan: Water is boiling\n";
  $pastaPan->fill(new Spaghetti());
  echo "pastaPan: Starting to boil spaghetti\n";
  $pastaPan->warm($duration = 8, function() use ($pastaPan, $plate, $water) {
    echo "pastaPan: Spaghetti is ready\n";
    $pastaPan->remove($water);
    $plate->addContentsOf($pastaPan);
  });
});

$eventLoop->executeLater($delay = 7, function() use ($plate, $eventLoop) {
  $saucePan = new AsynchronousPan($eventLoop);
  $saucePan->fill(new OliveOil());
  echo "saucePan: Starting to warm olive oil\n";
  $saucePan->warm($duration = 2, function() use($saucePan, $plate) {
    echo "saucePan: Olive oil is warm\n";
    $saucePan->fill(MirepoixFactory::create($withGarlic = true));
    echo "saucePan: Starting to cook the Mirepoix\n";
    $saucePan->warm($duration = 5, function() use($saucePan, $plate) {
      echo "saucePan: Mirepoix is ready to welcome tomato\n";
      $saucePan->fill(TomatoFactory::create());
      echo "saucePan: Starting to cook tomato\n";
      $saucePan->warm($duration = 4, function() use($saucePan, $plate) {
        echo "saucePan: Tomato sauce is ready\n";
        $plate->addContentsOf($saucePan);
      });
    });
  });
});

$eventLoop->start();

$plate->serve('Voilà');

Попросите PHP выполнить программу, и асинхронность начнет проявляться:

pastaPan: Starting to boil water
Tick is 1
...
Tick is 7
saucePan: Starting to warm olive oil
Tick is 8
Tick is 9
saucePan: Olive oil is warm
saucePan: Starting to cook the Mirepoix
Tick is 10
pastaPan: Water is boiling
pastaPan: Starting to boil spaghetti
Tick is 11
....
Tick is 14
saucePan: Mirepoix is ready to welcome tomato
saucePan: Starting to cook tomato
Tick is 15
...
Tick is 18
pastaPan: Spaghetti is ready
saucePan: Tomato sauce is ready

Awesomesauce! Все готово одновременно через 18 минут.

Примечание с одной стороны: Когда вы сравниваете первый и последний сценарии, вы сразу же узнаете визуальные различия. В первом скрипте все строки кода выровнены по вертикали; если вы хотите понять, что происходит последовательно, вам просто нужно читать сверху вниз. В последнем скрипте, с другой стороны, это отступ, который означает, что операторы выполняются один за другим. Чтобы следить за ходом программы, вы должны читать слева направо. В программировании, управляемом событиями, отступ означает последовательность.

Когда асинхронные обратные вызовы должны синхронизироваться

Есть только одна проблема. Планшет обслуживается в конце сценария, потому что цикл обработки событий заканчивается обратными вызовами. Но что, если сценарий должен был запланировать больше обратных вызовов для последующего выполнения, например, для мытья посуды? Тарелка будет подана очень поздно, с холодными спагетти. Это то, что вы никогда не должны делать. Это кощунство.

Таким образом, сценарий не должен полагаться на конец цикла событий. Это означает, что после $eventLoop->start()строки не должно быть кода . Но как синхронизировать тот факт, что пластина получает содержимое обеих сковородок, поскольку они работают в отдельных последовательностях?

Вы можете подождать, пока тик не достигнет 18, а затем подать тарелку, но это будет случайная ставка. Хотя асинхронные службы этого примера основаны на синхронизации, это редко имеет место. Большинство асинхронных обратных вызовов запускаются при возврате сторонней службы, такой как запрос к базе данных, открытие файла на диске или ответ на запрос HTTP. Вы никогда не знаете, когда это произойдет. Таким образом, вы должны положиться на новые методы ресинхронизации.

На случай спагетти с томатным соусом это довольно просто:

<?php
class PlateOfSpaghettiWithSauce extends Plate
{
  protected $hasSpaghetti = false;
  protected $hasSauce = false;

  public function addContentsOf(Pan $pan)
  {
    parent::addContentsOf($pan);
    if ($pan->contains('Spaghetti')) {
      $this->hasSpaghetti = true;
    }
    if ($pan->contains('Tomato')) {
      $this->hasSauce = true;
    }
    if ($this->hasSpaghetti && $this->hasSauce) {
      $this->serve('Voilà');
    }
  }
}

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

Там еще только один повар

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

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

В последовательных вычислениях потраченные впустую циклы ЦП данным потоком спасаются другими параллельными потоками. Вот почему при использовании PHP с Apache и mod_php вы определяете количество одновременных серверных процессов. Проблема в том, что каждый раз, когда вы создаете новый поток для восстановления потраченных циклов ЦП другим потоком, он потребляет много памяти. Вывод: чтобы использовать процессор так же эффективно, как программу, управляемую событиями, последовательной программе требуется намного больше памяти. И память является самой дорогой частью веб-серверов.

А как насчет Node.js?

Как Node.js сравнивается с PHP? Точно так же первый скрипт сравнивается с последним. Node.js продвигает программирование на основе событий и облегчает его. Во-первых, он опирается на JavaSript, который предоставляет расширенную систему событий. Во-вторых, Node предоставляет объект EventLoop и запускает его для вас в конце основного скрипта. Наконец, Node предоставляет новые (нативные) реализации для большинства блокирующих операций ввода-вывода, в которых используется асинхронность.

Например, чтобы удалить файл на диске с помощью Node, код выглядит следующим образом:

var fs = require('fs');
fs.unlink('/tmp/hello', function (err) {
  if (err) throw err;
  console.log('successfully deleted /tmp/hello');
});
// more code
console.log('deletion script');

Узел начинается с выполнения основного сценария до конца. unlink()Функция сообщает диск искать конкретный файл, удалить его, и обратный вызов узла , когда это будет сделано. Тем временем Node может продолжать выполнять больше кода. Поэтому сообщение «сценарий удаления» регистрируется до того, как удаление действительно произойдет. Когда он достигает конца скрипта, он запускает цикл обработки событий. Теперь он готов к приему сигнала от файловой системы, сигнализирующего о фактическом удалении, что инициирует выполнение анонимной функции, которая регистрирует сообщение «успешно удалено».

JavaScript также добавляет синтаксический сахар в замыкания PHP. Благодаря лексической области видимости нет необходимости упоминать useоператор при объявлении анонимной функции, поскольку JavaScript запоминает свое состояние между выполнениями. Итак, асинхронный вызов PHP:

<?php
$plate = new Plate();
$pastaPan = new AsynchronousPan($eventLoop);
$water = new Water();
$pastaPan->fill($water);
echo "pastaPan: Starting to boil water\n";
$pastaPan->warm($duration = 10, function() use ($pastaPan, $plate, $water) {
  echo "pastaPan: Water is boiling\n";
  $pastaPan->fill(new Spaghetti());
  echo "pastaPan: Starting to boil spaghetti\n";
  $pastaPan->warm($duration = 8, function() use ($pastaPan, $plate, $water) {
    echo "pastaPan: Spaghetti is ready\n";
    $pastaPan->remove($water);
    $plate->addContentsOf($pastaPan);
  });
});

Переводится в JavaScript как:

plate = new Plate();
pastaPan = new AsynchronousPan();
water = new Water();
pastaPan.fill(water);
console.log('pastaPan: Starting to boil water');
pastaPan.warm(duration = 10, function() {
  console.log('pastaPan: Water is boiling');
  pastaPan.fill(new Spaghetti());
  console.log('pastaPan: Starting to boil spaghetti');
  pastaPan.warm(duration = 8, function() {
    console.log('pastaPan: Spaghetti is ready');
    pastaPan.remove(water);
    plate.addContentsOf(pastaPan);
  });
});

Наконец, Node позволяет писать динамические серверы, в то время как PHP нужен отдельный сервер (например, Apache) для обработки необработанного HTTP-запроса. Это означает, что сервер Node остается в памяти и компилируется только один раз. По сравнению с mod_php, который загружает всю среду выполнения PHP и все необходимые сценарии для каждого HTTP-запроса, это большое преимущество.

Примечание : одна важная вещь, о которой должны знать PHP-программисты, это злая природа thisключевого слова. Я уже упоминал об этом в предыдущем посте .

Вывод

В целом, Node.js быстрее, чем PHP, и потребляет меньше памяти. Тем не менее, программирование узлов становится все более сложным по мере увеличения кодовой базы сервера, потому что тогда вам нужно синхронизировать вручную. Это делает Node подходящим для серверов малого и среднего размера, выполняющих простые операции — например, хорошо подходят API REST.

Для меня истинное значение Node, кроме цикла обработки событий, — это новая асинхронная реализация запросов к базе данных, файловых операций, HTTP-запросов и т. Д. Это чистое золото, особенно если вы понимаете, что другие реализации в основном тратят циклы ЦП.

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