Разработчики PHP всегда чего-то ждут. Иногда мы ждем запросов к удаленным сервисам. Иногда мы ожидаем, что базы данных вернут строки из сложного запроса. Разве не было бы замечательно, если бы мы могли делать другие вещи в течение всего этого ожидания?
Если вы написали несколько JS, вы, вероятно, знакомы с обратными вызовами и событиями DOM. И хотя у нас есть обратные вызовы в PHP, они работают не совсем так. Это благодаря функции, называемой цикл обработки событий.
Мы рассмотрим, как работает цикл обработки событий и как мы можем использовать цикл обработки событий в PHP.
Мы собираемся увидеть некоторые интересные библиотеки PHP. Некоторые считают, что они еще недостаточно стабильны для использования в производстве. Некоторые считают, что представленные примеры «лучше делать на более зрелых языках». Есть веские причины, чтобы попробовать эти вещи. Есть также веские причины избегать этих вещей в производстве. Цель этого поста — показать, что возможно в PHP.
Куда идут дела
Чтобы понять циклы событий, давайте посмотрим, как они работают в браузере. Посмотрите на этот пример:
function fitToScreen(selector) { var element = document.querySelector(selector); var width = element.offsetWidth; var height = element.offsetHeight; var top = "-" + (height / 2) + "px"; var left = "-" + (width / 2) + "px"; var ratio = getRatio(width, height); setStyles(element, { "position": "absolute", "left": "50%", "top": "50%", "margin": top + " 0 0 " + left, "transform": "scale(" + ratio + ", " + ratio + ")" }); } function getRatio(width, height) { return Math.min( document.body.offsetWidth / width, document.body.offsetHeight / height ); } function setStyles(element, styles) { for (var key in styles) { if (element.style.hasOwnProperty(key)) { element.style[key] = styles[key]; } } } fitToScreen(".welcome-screen");
Этот код не требует дополнительных библиотек. Он будет работать в любом браузере, который поддерживает преобразования масштаба CSS. Последняя версия Chrome должна быть всем, что вам нужно. Просто убедитесь, что селектор CSS соответствует элементу в вашем документе.
Эти несколько функций занимают селектор CSS и центрируют и масштабируют элемент по размеру экрана. Что произойдет, если мы добавим Error
внутри цикла for
? Мы бы увидели что-то вроде этого …
Мы называем этот список функций трассировкой стека. Это то, что выглядит внутри стековых браузеров. Они будут обрабатывать этот код поэтапно …
Это похоже на то, как PHP использует стек для хранения контекста. Браузеры идут дальше и предоставляют WebAPI для таких вещей, как события DOM и обратные вызовы Ajax. В своем естественном состоянии JavaScript так же асинхронен, как и PHP. То есть: хотя оба выглядят так, как будто они могут делать много вещей одновременно, они являются однопоточными. Они могут делать только одну вещь одновременно.
С помощью WebAPI браузера (такие как setTimeout и addEventListener) мы можем перенести параллельную работу в разные потоки. Когда эти события происходят, браузеры добавляют обратные вызовы в очередь обратных вызовов. Когда стек в следующий раз пуст, браузеры выбирают обратные вызовы из очереди обратных вызовов и выполняют их.
Этот процесс очистки стека, а затем очереди обратного вызова является циклом обработки событий.
Жизнь без событий цикла
В JS мы можем запустить следующий код:
setTimeout(function() { console.log("inside the timeout"); }, 1); console.log("outside the timeout");
Когда мы запускаем этот код, мы видим outside the timeout
а затем inside the timeout
в консоли. Функция setTimeout
является частью WebAPI, с которыми нам позволяют работать браузеры. По истечении 1 миллисекунды они добавляют обратный вызов в очередь обратного вызова.
Второй console.log
завершается до запуска изнутри setTimeout
. У нас нет ничего похожего на setTimeout
в стандартном PHP, но если бы нам пришлось попробовать и смоделировать это:
function setTimeout ( callable $callback , $delay ) { $now = microtime ( true );
while ( true ) {
if ( microtime ( true ) - $now > $delay ) { $callback ();
return ;
}
}
} setTimeout ( function () {
print "inside the timeout" ;
}, 1 );
print "outside the timeout" ;
Когда мы запускаем это, мы видим inside the timeout
а затем outside the timeout
. Это потому, что мы должны использовать бесконечный цикл внутри нашей функции setTimeout
для выполнения обратного вызова после задержки.
Может быть заманчиво переместить цикл while за пределы setTimeout
и обернуть в него весь наш код. Это может сделать наш код менее блокирующим, но в какой-то момент мы всегда будем блокироваться этим циклом. В какой-то момент мы увидим, что мы не можем сделать больше, чем одну вещь в одном потоке за один раз.
Хотя в стандартном PHP нет ничего похожего на setTimeout
, есть несколько неясных способов реализации неблокирующего кода наряду с циклами событий. Мы можем использовать такие функции, как stream_select
для создания неблокирующего сетевого ввода-вывода. Мы можем использовать расширения C, такие как EIO, для создания неблокирующего кода файловой системы. Давайте посмотрим на библиотеки, построенные на этих малоизвестных методах …
Сосулька
Icicle — это библиотека компонентов, созданная с учетом цикла событий. Давайте посмотрим на простой пример:
use Icicle \Loop ;
Loop \timer ( 0.1 , function () {
print "inside timer" ;
});
print "outside timer" ;
Loop \run ();
Это с icicleio/icicle
версии 0.8.0
.
Реализация цикла событий Icicle великолепна. У этого есть много других впечатляющих особенностей; как A + обещания, сокеты и реализации сервера.
Сосулька также использует генераторы в качестве сопрограмм. Генераторы и сопрограммы — это отдельная тема, но код, который они допускают, прекрасен:
use Icicle \Coroutine ;
use Icicle \Dns\Resolver\Resolver ;
use Icicle \Loop ; $coroutine = Coroutine \create ( function ( $query , $timeout = 1 ) { $resolver = new Resolver (); $ips = ( yield $resolver -> resolve ( $query , [ "timeout" => $timeout ]
));
foreach ( $ips as $ip ) {
print "ip: {$ip}\n" ;
}
}, "sitepoint.com" );
Loop \run ();
Это с icicleio/dns
версии 0.5.0
.
Генераторы облегчают написание асинхронного кода способом, похожим на синхронный код. В сочетании с обещаниями и циклом обработки событий они приводят к большому неблокирующему коду, как этот!
ReactPHP
ReactPHP имеет аналогичную реализацию цикла событий, но без всех интересных вещей генератора:
$loop = React \EventLoop\Factory :: create (); $loop -> addTimer ( 0.1 , function () {
print "inside timer" ;
});
print "outside timer" ; $loop -> run ();
Это с 0.4.1
react/event-loop
0.4.1
react/event-loop
версии 0.4.1
.
ReactPHP более зрелый, чем сосулька, и имеет больший набор компонентов. У Icicle есть путь, прежде чем он сможет бороться со всеми функциями, которые предлагает ReactPHP. Разработчики делают хорошие успехи, хотя!
Вывод
Трудно выбраться из однопоточного мышления, которое нас учат. Мы просто не знаем границ кода, который мы могли бы написать, если бы у нас был доступ к неблокирующим API и циклам событий.
Сообщество PHP должно осознать эту архитектуру. Нам нужно учиться и экспериментировать с асинхронным и параллельным выполнением. Мы должны скопировать эти концепции и лучшие практики из других языков, в которых циклы событий были целую вечность, до тех пор, пока «как я могу эффективно использовать большинство системных ресурсов?» — это простой вопрос для ответа на PHP.
Оставайтесь с нами для более практичной реализации Icicle, которая скоро появится!