PHP не поддерживает обработку потоков. Несмотря на это, и в противоположность мнению большинства PHP-разработчиков, с которыми я общался, PHP-приложения могут быть многозадачными. Начнем с выяснения того, что "многозадачность" и "поточность" означают для PHP-программирования.
Сначала отложим в сторону случаи, лежащие вне русла главной темы. У PHP сложные взаимоотношения с многозадачностью или параллелизмом. На верхнем уровне PHP постоянно вовлечен в многозадачность - стандартные установки PHP на сервере (например, модуль Apache) используются многозадачным способом. То есть несколько клиентских приложений (Web-браузеров) могут одновременно запросить одну и ту же PHP-страницу, и Web-сервер возвратит ее всем более или менее одновременно.
Одна Web-страница не блокирует передачу другой, хотя они могут немного мешать друг другу при работе с такими ограниченными ресурсами как память сервера или пропускная способность сети. Таким образом, системное требование обеспечения параллелизма может вполне допускать основанные на PHP решения. В терминах реализации PHP возлагает на Web-сервер ответственность за параллелизм.
Параллелизм на клиентской стороне под названием Ajax тоже привлек внимание разработчиков в последние несколько лет. Хотя значение Ajax стало несколько неясным, одним из аспектов этой технологии является то, что браузер может одновременно выполнять вычисления и оставаться чувствительным к таким действиям пользователя, как выбор пунктов меню. Это действительно отчасти многозадачность. Закодированный на PHP Ajax делает это, но без какого-либо специального участия PHP; интегрированные среды Ajax для других языков работают точно также.
Третьим примером параллелизма, который только поверхностно затрагивает PHP, является PHP/TK. PHP/TK - это расширение PHP, предоставляющее переносимые связывания графического интерфейса пользователя (Graphical User Interface - GUI) ядру PHP. PHP/TK позволяет создавать настольные GUI-приложения, написанные на PHP. Его основанные на событиях аспекты моделируют форму параллелизма, которую легко изучить, и она меньше подвержена ошибкам, чем работа с потоками. Опять же, параллелизм "унаследован" от дополнительной технологии, а не является фундаментальной функциональностью PHP.
Было несколько экспериментов по добавлению поддержки поточности в сам PHP. Насколько я знаю, ни один не был удачным. Однако ориентированные на события интегрированные среды Ajax и PHP/TK показывают, что события могут еще лучше выразить параллелизм для PHP, чем это делают потоки. PHP V5 доказывает это.
PHP V5 предлагает stream_select()
В стандартном PHP V4 и более ранних версиях вся работа PHP-приложения должна выполняться последовательно. Если программе нужно извлечь цену товара с двух коммерческих сайтов, например, она запрашивает первую цену, ждет получения ответа, запрашивает вторую цену и ждет опять.
Что, если бы программа могла выполнять несколько задач одновременно? Она завершалась бы лишь за часть того времени, которое необходимо при последовательной работе.
Новая функция stream_select, вместе с несколькими своими друзьями, предоставляет эту возможность. Рассмотрим следующий пример:
Листинг 1. Одновременный запрос нескольких HTTP-страниц
<?php
echo "Program starts at ". date('h:i:s') . ".\n";
$timeout=10;
$result=array();
$sockets=array();
$convenient_read_block=8192;
/* Выполнить одновременно все запросы; ничего не блокируется. */
$delay=15;
$id=0;
while ($delay > 0) {
$s=stream_socket_client("phaseit.net:80", $errno,
$errstr, $timeout,
STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT);
if ($s) {
$sockets[$id++]=$s;
$http_message="GET /demonstration/delay?delay=" .
$delay . " HTTP/1.0\r\nHost: phaseit.net\r\n\r\n";
fwrite($s, $http_message);
} else {
echo "Stream " . $id . " failed to open correctly.";
}
$delay -= 3;
}
while (count($sockets)) {
$read=$sockets;
stream_select($read, $w=null, $e=null, $timeout);
if (count($read)) {
/* stream_select обычно перемешивает $read, поэтому мы должны вычислить,
из какого сокета выполняется чтение. */
foreach ($read as $r) {
$id=array_search($r, $sockets);
$data=fread($r, $convenient_read_block);
/* Сокет можно прочитать либо потому что он
имеет данные для чтения, ЛИБО потому что он в состоянии EOF. */
if (strlen($data) == 0) {
echo "Stream " . $id . " closes at " . date('h:i:s') . ".\n";
fclose($r);
unset($sockets[$id]);
} else {
$result[$id] .= $data;
}
}
} else {
/* Таймаут означает, что *все* потоки не
дождались получения ответа. */
echo "Time-out!\n";
break;
}
}
?>
|
Если выполнить эту программу, отобразится примерно следующая информация:
Листинг 2. Типичная информация, выводимая программой из листинга 1
Program starts at 02:38:50.
Stream 4 closes at 02:38:53.
Stream 3 closes at 02:38:56.
Stream 2 closes at 02:38:59.
Stream 1 closes at 02:39:02.
Stream 0 closes at 02:39:05.
|
Важно понимать, что здесь происходит. На высоком уровне первая программа выполняет несколько HTTP-запросов и получает страницы, которые передает ей Web-сервер. Хотя реальное приложение, наверное, запрашивало бы несколько различных Web-серверов (возможно google.com, yahoo.com, ask.com и т.д.), этот пример передает все запросы на наш корпоративный сервер на Phaseit.net просто ради уменьшения сложности.
Запрошенные Web-страницы возвращают результаты после переменной задержки, показанной ниже. Если бы программа выполняла запросы последовательно, для ее завершения понадобилось бы около 15+12+9+6+3 (45) секунд. Как показано в листинге 2, на самом деле она завершается за 15 секунд. Утроение производительности - это отличный результат.
Такое стало возможно благодаря stream_select - новой функции в PHP V5. Запросы инициируются обычным способом - открытием нескольких stream_socket_clients и написанием GET к каждому из них, что соответствует http://phaseit.net/demonstration/delay?delay=$DELAY. При запросе этого URL в браузере вы должны увидеть:
Starting at Thu Apr 12 15:05:01 UTC 2007.
Stopping at Thu Apr 12 15:05:05 UTC 2007.
4 second delay.
|
Сервер задержки реализован на CGI, как показано ниже.
Листинг 3. Реализация сервера задержки
#!/bin/sh
echo "Content-type: text/html
<HTML> <HEAD></HEAD> <BODY>"
echo "Starting at `date`."
RR=`echo $REQUEST_URI | sed -e 's/.*?//'`
DELAY=`echo $RR | sed -e 's/delay=//'`
sleep $DELAY
echo "<br>Stopping at `date`."
echo "<br>$DELAY second delay.</body></html>"
|
Хотя конкретная реализация в листинге 3 предназначена для UNIX®, почти все сценарии данной статьи с тем же успехом применимы для установок PHP в Windows® (особенно после Windows 98) или UNIX. В частности, с листингом 1 можно работать на любой операционной системе. Linux® и Mac OS X являются вариациями UNIX, и весь приведенный здесь код будет работать в обеих системах.
Запросы к серверу задержки выполняются в следующем порядке:
Листинг 4. Последовательность выполнения процесса
delay=15
delay=12
delay= 9
delay= 6
delay= 3
|
Целью stream_select является как можно более быстрое получение результатов. В данном случае порядок задержек противоположен порядку, в котором были сделаны запросы. Через 3 секунды первая страница готова для чтения. Эта часть программы является обычным PHP-кодом - в данном случае с fread. Также как и в другой PHP-программе чтение могло бы осуществляться при помощи fgets.
Обработка продолжается таким же образом. Программа блокируется в stream_select, пока не будут готовы данные. Решающим является то, что она начинает чтение, как только какое-либо соединение будет иметь данные, в любом порядке. Именно так программа реализует многозадачность или параллельную обработку результатов нескольких запросов.
Обратите внимание на то, что при этом нет дополнительной нагрузки на CPU хост-компьютера. Нет ничего необычного в том, что сетевые программы, выполняющие fread таким способом, вскоре начинают использовать 100% мощности CPU. Здесь не этот случай, поскольку stream_select имеет желаемые свойства и отвечает немедленно, как только какое-нибудь чтение становится возможным, но при этом минимальным образом загружает CPU в режиме ожидания между операциями чтения.
Что нужно знать о stream_select()
Подобное основанное на событиях программирование не является элементарной задачей. Хотя листинг 1 и уменьшен до самых основных моментов, любое кодирование, базирующееся на обратных вызовах или координации (что является необходимым в многозадачных приложениях) будет менее привычным по сравнению с простой процедурной последовательностью. В данном случае наибольшая трудность заключена в массиве $read. Обратите внимание на то, что это ссылка; stream_select возвращает важную информацию путем изменения содержимого $read. Так же как указатели имеют репутацию постоянного источника ошибок в C, ссылки, по-видимому, являются той частью PHP, которая представляет наибольшую трудность для программистов.
Такую методику запросов можно использовать из любого числа внешних Web-сайтов, удостоверяясь в том, что программа будет получать каждый результат как можно быстрее, не ожидая других запросов. Фактически, данная методика корректно работает с любым TCP/IP-соединением, а не только с Web (порт 80), то есть в принципе вы можете управлять извлечением LDAP-данных, передачей SMTP, SOAP-запросами и т.д.
Но это не все. PHP V5 управляет различными соединениями как "потоками" (stream), а не простыми сокетами. Библиотека PHP Client URL (CURL) поддерживает HTTPS-сертификаты, исходящую FTP-загрузку, куки и многое другое (CURL позволяет PHP-приложениям использовать различные протоколы для соединения с серверами). Поскольку CURL предоставляет интерфейс stream, с точки зрения программы соединение прозрачно. В следующем разделе рассказывается, как stream_select мультиплексирует даже локальные вычисления.
Для stream_select существует несколько предостережений. Эта функция не документирована, поэтому не рассматривается даже в новых книгах по PHP. Несколько примеров кода, доступные в Web, просто не работают или не понятны. Второй и третий аргументы stream_select, управляющие каналами write и exception, соответствующими каналам read в листинге 1, почти всегда должны быть равны null. За некоторыми исключениями выбор этих каналов является ошибкой. Если вы не имеете достаточного опыта, используйте только хорошо описанные варианты.
Кроме того, в stream_select, по всей видимости, имеются ошибки, по крайней мере, в PHP V5.1.2. Наиболее существенным является то, что значению возврата функции нельзя доверять. Хотя я еще не отладил реализацию, мой опыт показал, что безопасно тестировать count($read) так, как в листинге 1, но это не относится к значению возврата самой stream_select, несмотря на официальную документацию.
Пример и основная часть обсуждения выше были посвящены тому, как управлять несколькими удаленными ресурсами одновременно и получать результаты по мере их появления, а не ожидать обработки каждого в порядке первоначальных запросов. Это, несомненно, важное применение параллелизма PHP. Иногда реальные приложения можно ускорить в десять и более раз.
Что если замедление происходит поближе? Есть ли способ ускорить получение результатов в PHP при локальной обработке? Есть несколько. Пожалуй, они еще менее известны, чем ориентированный на сокеты подход в листинге 1. Этому есть несколько причин, в том числе:
- В своем большинстве PHP-страницы достаточно быстры. Лучшая производительность могла бы быть преимуществом, но этого недостаточно для оправдания инвестиций в новый код.
- Использование PHP в Web-страницах может сделать частичные ускорения кода не важными. Перераспределение вычислений таким образом, чтобы получать промежуточные результаты быстрее, не имеет значения, когда единственным критерием является скорость доставки Web-страницы в целом.
- Немного локальных узких мест находится под контролем PHP. Пользователи могут выражать недовольство тем, что извлечение информации об учетной записи занимает 8 секунд, но это может быть ограничением обработки базы данных или каких-либо других ресурсов, внешних для PHP. Даже если уменьшить время PHP-обработки до нуля, все равно будет затрачено более 7 секунд просто на поиск.
- Еще меньшее количество ограничений поддается параллельной обработке. Предположим, что конкретная страница вычисляет рекомендуемую цену для перечисленных обыкновенных акций, а вычисления достаточно сложны и выполняются в течение многих секунд. Вычисление может быть последовательным по природе. Не существует очевидного способа распределить его для "совместной работы".
- Мало PHP-программистов понимает потенциал PHP для реализации параллельной обработки. Говоря о возможности распараллеливания, большинство из встреченных мной программистов просто цитировали фразу "PHP не работает с потоками" и возвращались к своей сложившейся модели вычислений.
Иногда можно добиться большего. Предположим, что PHP-страница должна вычислить два биржевых курса, возможно, сравнить их, а используемый хост-компьютер является многопроцессорным. В данном случае мы можем почти удвоить производительность, назначив два отдельных, выполняющихся продолжительное время вычисления различным процессорам.
В мире PHP-вычислений такие примеры являются редкостью. Однако поскольку я больше нигде не нашел точного описания, хочу привести здесь пример подобного ускорения.
Листинг 5. Реализация сервера задержек
<?php
echo "Program starts at ". date('h:i:s') . ".\n";
$timeout=10;
$streams=array();
$handles=array();
/*Сначала запустить программу с задержкой в 3 секунды, затем
ту, которая возвращает результат после одной секунды. */
$delay=3;
for ($id=0; $id <= 1; $id++) {
$error_log="/tmp/error" . $id . ".txt"
$descriptorspec=array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("file", $error_log, "w")
);
$cmd='sleep ' . $delay . '; echo "Finished with delay of ' .
$delay . '".';
$handles[$id]=proc_open($cmd, $descriptorspec, $pipes);
$streams[$id]=$pipes[1];
$all_pipes[$id]=$pipes;
$delay -= 2;
}
while (count($streams)) {
$read=$streams;
stream_select($read, $w=null, $e=null, $timeout);
foreach ($read as $r) {
$id=array_search($r, $streams);
echo stream_get_contents($all_pipes[$id][1]);
if (feof($r)) {
fclose($all_pipes[$id][0]);
fclose($all_pipes[$id][1]);
$return_value=proc_close($handles[$id]);
unset($streams[$id]);
}
}
}
?>
|
Данная программа выведет на экран следующую информацию:
Program starts at 10:28:41.
Finished with delay of 1.
Finished with delay of 3.
|
Смысл заключается в том, что PHP запустил два независимых субпроцесса, получил данные от первого, а затем от второго, хотя последний стартовал раньше. Если хост-компьютер является многопроцессорным, а операционная система корректно настроена, она сама заботится о назначении различных субпрограмм разным процессорам. Это один из способов использования преимуществ многопроцессорных машин в PHP.
PHP поддерживает многозадачность. PHP не поддерживает обработку потоков так, как это делают другие языки программирования, например Java™ или C++, но приведенные выше примеры показали, что PHP имеет более высокий потенциал для ускорения работы, чем многие себе представляют.
Научиться
- Оригинал статьи "Develop multitasking applications with PHP V5" (EN).
- "Руководство по миграции на PHP V5" - информация о новейшей версии, которую должны знать все PHP-программисты (EN).
- PHP частично поддерживает "Process Control Functions", которые были предпочтительным способом программирования параллельной обработки во времена PHP V4 (EN).
- В PHP V5 функция
stream_selectи связанные с ней функции вытеснили PCNTL для параллельной обработки (EN). -
curl_multi_select- недокументированная функция PHP V5, поддерживающая такой же стиль программирования выборки, представленный в данной статье, и обеспечивающая полную функциональность CURL (EN). - Выдающийся ученый Джон Остераут (John Ousterhout) в 1996 году сделал доклад по теме "Почему потоки являются плохой идеей (для большинства задач)" (EN).
-
Вез Фурлонг (Wez Furlong) реализовал основную часть PHP-потоков, используемых в данной статье (EN).
- Согласно Web-сайту PHP/TK, "PHP/TK - это родное расширение языка программирования PHP, которое значительно упрощает написание клиентских кросс-платформенных GUI-приложений" (EN).
- Самый актуальный список доступных интегрированных сред Ajax для PHP (EN).
-
PHP.net - центральный ресурс для PHP-разработчиков (EN).
- "Рекомендованный список для чтения по языку PHP" (EN).
-
Информация по PHP на сайте developerWorks .
- Усовершенствуйте свои навыки в PHP-программировании, используя ресурсы IBM developerWorks PHP project (EN).
-
Вещательные программы developerWorks - интересные интервью и дискуссии для разработчиков программного обеспечения (EN).
- Следите за техническими событиями и web-трансляциями на developerWorks (EN).
- Используете базы данных в PHP? Обратите внимание на Zend Core for IBM, цельную, готовую к использованию, легкую в установке интегрированную среду разработки на PHP, поддерживающую IBM DB2 V9 (EN).
- Следите за проходящими по всему миру конференциями, выставками, web-трансляциями и другими событиями, интересными для разработчиков систем IBM с открытым исходным кодом (EN).
-
Раздел developerWorks Open source с исчерпывающей информацией how-to, инструментальными средствами и обновлениями проектов, помогающей использовать в разработке технологии с открытым исходным кодом и продукты IBM .
- Информация об IBM технологиях с открытым исходным кодом, функциях продуктов в бесплатных демонстрационных материалах developerWorks On demand (EN).
Получить продукты и технологии
- Разработайте ваш следующий проект с открытым исходным кодом, используя пробное программное обеспечение IBM, доступное для загрузки или на DVD.(EN)
- Загрузите оценочные версии продуктов IBM и используйте инструментальные средства разработки приложений и программы промежуточного уровня DB2®, Lotus®, Rational®, Tivoli® и WebSphere®.
Обсудить
- Примите участие в обсуждении материала на форуме.
- Принимайте участие в блогах developerWorks и подключайтесь к сообществу developerWorks.(EN)
- Принимайте участие в форуме developerWorks PHP: Разработка PHP-приложений с использованием продуктов IBM Information Management (DB2, IDS).(EN)

Кэмерон Лэйрд (Cameron Laird) - бывший обозревательэтого сайта и в течение длительного времени пишет для developerWorks. Он часто рассказывает про Open Source проекты, позволяющие его работодателям ускорить разработку технологий в области надежности и безопасности передачи информации. Кэмерон впервые начал использовать AIX двадцать лет назад, когда тот был все еще экспериментальным продуктом. Все это время Кэмерон был заинтересованным пользователем и разработчиком средств для отладки памяти. Вы можете связаться с ним по адресу claird@phaseit.net.