[Эту мини-статью я когда-то написал для журнала "International PHP Magazine", как часть колонки "Спроси гуру". Я перепечатываю ее здесь, потому что она полезная и потому что люди просили меня об этом дважды за последние два дня]
Вопрос:
Существует ли в PHP хоть какая-то многопоточность?
Скажем, вы написали PHP-приложения для мониторинга служб на некотором количестве серверов и было бы неплохо запрашивать несколько серверов одновременно, а не один за одним.
Это возможно?
Ответ:
Люди часто предполагают, что необходимо разветвлять или порождать потоки, когда понадобится выполнять несколько действий одновременно, и если к тому же приложение реализовано на PHP (а этот язык не поддерживает многопоточность), то они должны перейти на что-то другое более подходящее, например perl.
У меня для вас хорошая новость - в большинстве случаев вам не нужно порождать и создавать новых потоков вообще и можно получить отличную производительность и без этого.
Скажем, Вам нужно проверять веб-серверы, действительно ли они рабочие в данный момент. Вы можете написать следующий скрипт:
< ?php
$hosts = array("host1.sample.com", "host2.sample.com");
$timeout = 15;
$status = array();
foreach ($hosts as $host) {
$errno = 0;
$errstr = "";
$s = fsockopen($host, 80, $errno, $errstr, $timeout);
if ($s) {
$status[$host] = "Соединение установлено\n";
fwrite($s, "HEAD / HTTP/1.0\r\nHost: $host\r\n\r\n");
do {
$data = fread($s, 8192);
if (strlen($data) == 0) {
break;
}
$status[$host] .= $data;
} while (true);
fclose($s);
} else {
$status[$host] = "Соединение прервано: $errno $errstr\n";
}
}
print_r($status);
?>
И этот скрипт будет работать отлично, но так как функция fsockopen() не возвращает управление до тех пор, пока не получит имя хоста и не установит соединение (или она будет ждать таймаут в $timeout секунд), то использовать этот сценарий для мониторинга большого количества хостов не получится в виду его медленности.
Нет никакой причины, почему мы должны делать это последовательно; мы можем открывать асинхронные соединения - то есть, соединения, где мы не должны ждать возврата из функции fsockopen(). PHP все еще будет определять имя хоста (так что лучше использовать IP-адреса), но управление будет возвращено в программу как только будет запущено открытие соединения, таким образом мы сможеи перейти к следующему хосту.
Есть два способа сделать это; в PHP 5, вы можете использовать функцию stream_socket_client() в качестве замены fsockopen(). В более ранних версиях PHP, Вам придется поработать ручками и воспользоваться расширением по работе с сокетами.
Вот как это делается в PHP 5:
< ?php
$hosts = array("host1.sample.com", "host2.sample.com");
$timeout = 15;
$status = array();
$sockets = array();
/* Инициируем соединения ко всем хостам одновременно */
foreach ($hosts as $id => $host) {
$s = stream_socket_client("$host:80", $errno, $errstr, $timeout,
STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT);
if ($s) {
$sockets[$id] = $s;
$status[$id] = "in progress";
} else {
$status[$id] = "failed, $errno $errstr";
}
}
/* Теперь ожидаем результат */
while (count($sockets)) {
$read = $write = $sockets;
/* Вот она - магическая функция - пояснения ниже */
$n = stream_select($read, $write, $e = null, $timeout);
if ($n > 0) {
/* доступные для чтения сокеты готовы отдать нам данные
или попытка провалилась
*/
foreach ($read as $r) {
$id = array_search($r, $sockets);
$data = fread($r, 8192);
if (strlen($data) == 0) {
if ($status[$id] == "in progress") {
$status[$id] = "failed to connect";
}
fclose($r);
unset($sockets[$id]);
} else {
$status[$id] .= $data;
}
}
/* доступные для записи сокеты могут принимать
HTTP-запросы
*/
foreach ($write as $w) {
$id = array_search($w, $sockets);
fwrite($w, "HEAD / HTTP/1.0\r\nHost: "
. $hosts[$id] . "\r\n\r\n");
$status[$id] = "waiting for response";
}
} else {
/* ожидаем таймаут; подразумевается, что все сокеты,
ассоциированные с массивом $sockets не сработали
*/
foreach ($sockets as $id => $s) {
$status[$id] = "timed out " . $status[$id];
}
break;
}
}
foreach ($hosts as $id => $host) {
echo "Host: $host\n";
echo "Status: " . $status[$id] . "\n\n";
}
?>
Мы используем функцию stream_select() для ожидания возникновения событий на открытых сокетах. stream_select() вызывает системную функцию select(2), а она работает так: первые три параметра - это массивы потоков, с которыми Вы хотите работать; Вы можете ожидать готовности чтения, записи или исключительных событий (параметр первый, второй и третий соответственно). stream_select() будет ждать $timeout секунд пока событие не появится - когда же эот случится, функция будет модифицировать массиы, которые Вы ей передали, так что они будут содержать идентификаторы сокетов, удовлетворябщих Вашему критерию.
Теперь, используя PHP 4.1.0 или более позднюю версию, если она скомпилирована с поддержкой расширения для работы с сокетами (sockets extension), Вы сможете использовать скрипт, который упомянут выше, но Вы должны заменить вызовы функций для работы с обычными потоками/файловой системой их эквивалентами из расширения sockets. Главная разница в способе открытия соединения; вместо stream_socket_client(), Вам необходимо использовать эту функцию:
< ?php
// Это значение верно для Linux,
// для других систем используйте другие значения
define('EINPROGRESS', 115);
function non_blocking_connect($host,$port,&$errno,&$errstr,$timeout) {
$ip = gethostbyname($host);
$s = socket_create(AF_INET, SOCK_STREAM, 0);
if (socket_set_nonblock($s)) {
$r = @socket_connect($s, $ip, $port);
if ($r || socket_last_error() == EINPROGRESS) {
$errno = EINPROGRESS;
return $s;
}
}
$errno = socket_last_error($s);
$errstr = socket_strerror($errno);
socket_close($s);
return false;
}
?>
Теперь, замените stream_select() на socket_select(), fread() на socket_read(), fwrite() на socket_write() и fclose() на socket_close() и вы готовы запускать сценарий.
Преимущество PHP 5 в том, что вы сможете использовать функцию stream_select() для ожидания данных (почти!) из любого типа потока - вы даже сможете использовать ее для ожидания ввода с клавиатуры терминала, включив STDIN в массив для чтения, или ожидать данные из каналов, созданных с помощью proc_open().
Если Вы пользуете PHP 4.3.x и хотите воспользоваться native streams approach, я приготовил патч, который позволяет работать функции fsockopen() асинхронно. Патч не поддерживается и не будет поставляться в официальном релизе PHP, однако, я написал оболочку, которая реализует функцию stream_socket_client() наряду с патчем, поэтому ваш код будет совместим с PHP 5.
Ресурсы:
Документация по stream_select
Документация по socket_select
Патч для PHP 4.3.2 и скрипт для эмуляции stream_socket_client() (должен работать и с более поздними версиями PHP)
Автор: Уэз Фёлонг (Wez Furlong)
Перевод: Алексей Тимков aka Bambino
Оригинал статьи
Распечатать статью
18 Responses
Bambino
March 20th, 2007 at 11:22
1Хочу добавить от себя, что в данном коде присутствует одна неточность, которая становится заметной только после прочтения документации по socket_select.
А именно, в строке:
$n = stream_select($read, $write, $e = null, $timeout);массивы $read и $write должны содержать ТОЛЬКО ТЕ идентификаторы сокетов, изменение состояния которых Вы ожидаете. Не помещайте туда ВСЕ сокеты, иначе ответ функции socket_select может быть непредсказуем.
FL
July 25th, 2007 at 11:15
2Hi, do you use curl multi libraty? Thanks.
Bambino
July 25th, 2007 at 12:39
3FL: Unfortunatelly I didn’t do this. I used only curl. For curl_multi PHP5 is required and I had no it. at that time (Для тех, кто не понял, я написал, что не пользовал эту либу.
)
GTAlex
September 6th, 2007 at 09:41
4ещё можно добавить - на 5ом пхп мультикурл присутствует
Bambino
September 6th, 2007 at 10:27
5Ну так как на момент написания мной мультипоточного скрипта у меня не было 5-го ПХП, то пришлось довольствоваться socket’ами.
Валентин
January 31st, 2008 at 17:17
65 php рулит!
Bambino
February 1st, 2008 at 09:23
7Валентин, может и рулит.. да только задача стояла сделать это на 4-ом. Кстати, от перемены мест слагаемых сумма не меняется… я не думаю, что сокетные функции работают как-то по-другому в 5-м php…
Николай
February 5th, 2008 at 08:34
8Всё хорошо работает. Как говорится , плохому танцору …. мешают.
Organic Baby Bedding
May 16th, 2008 at 21:55
9Organic Baby Bedding…
I found your site on technorati and read a few of your other posts. Keep up the good work. I just added your RSS feed to my Google News Reader. Looking forward to reading more from you….
Bambino
May 17th, 2008 at 01:26
10Spammers go home!
Alexk
July 5th, 2008 at 04:04
11используя данный код можно нарваться на медленные или дохлые страницы и тогда весь процесс зависнет и вылетит по таймауту, что не желательно. Я использовал AJAX и асинхронные запросы - описание в моём блоге
Bambino
July 5th, 2008 at 16:29
12А что, ajax не подвержен воздействию медленных и дохлых страниц?
Или соединения осуществляются без использования сокетов? И вообще, при чем тут ajax?!! Речь идет о многопоточности в PHP!!
Многопоточности в PHP через AJAX просто не может быть лишь потому, что AJAX - это не часть PHP и даже не его надстройка. Это технология, которая реализуется с помощью серверного скриптового языка, например, того же PHP.
Да и название твоего поста “Многопоточность в PHP через AJAX” наводит на мысль о том, что кто-то не до конца понимает, что такое PHP, а что такое AJAX
Alexk
July 5th, 2008 at 17:49
13Пойми, многопоточности в PHP нет!!! А та, что “есть” именно ведёт к проблеме дохлых сокетов. Если ты внимательно читал мой пост в блоге, то должен был понять, что каждый ответ от скрипта-парсера обрабатывается отдельно, а не пачкой, как например в curl.
Насчёт применения AJAX - это выход, который я нашёл и пока доволен им на все 100%
Bambino
July 6th, 2008 at 00:42
14Ну только не нужно говорить таких громких слов.
Ты пишешь, что многопоточности в PHP нет, но твой пост содержит в себе это выражение - ты противоречишь сам себе :)… И еще раз повторюсь многопоточность в PHP никак не может быть выполнена через ajax. Согласен? PHP - это серверный язык (ты это понимаешь), а ajax - это технология никак с PHP не связана.
Я сам лично создал скрипт, который обращался к нескольким серверам одновременно и все отлично работало. То, что сделал ты, другие люди делают с помощью fork’ов и согласись, что этот метод даже более действенен, чем твой. Кстати, PHPшный socket_select - это не более, чем обертка юниксовой функции select. Это не какой-то там AJAX, который тупо перестанет работать, когда пользователь запретить выполнение JavaScript.
Понимаешь, можно и из пушки по воробьям стрелять. Каждый должен выполнять свои функции. PHP работать с сокетами, AJAX работать с содержанием страницы. Кстати функция file_get_contents блокирующая, а это потенциальная проблема, т.е. потенциально ты можешь повесить все свои AJAX-”потоки”. А мы люди серьезные, не выдумываем странные идеи, а работаем с неблокирующими сокетами, для чего имеются все необходимые функции.
Функция socket_get_option сообщит тебе о статусе сокета и в socket_select попадут только рабочие сокеты! Никаких блокировок!
Возможно слово multiplexing и не переводится как многопоточность, но в применении к тесту этого поста (ты же его внимательно читал?), этот термин можно применить. Здесь под многопоточностью понимается асинхронная работа с несколькими неблокирующими сокетами. И socket_select отлично справляется со своей работой! И не нужно так категорично заявлять. Как говорят, вы просто не умеете их готовить.
P.S. PHP-шный парсер можно закинуть на отдельный сервер, запустить как процесс или несколько процессов(именно процессов!), можно заставить работать через прокси (любые). А что можно сделать с твоим? Разве, что для твоих нужд. Нет - это не наш метод. Телефон должен звонить, фотоаппарат фотографировать. Согласись, что фотокамеры в мобильниках отстой…
VolCh
July 16th, 2008 at 02:22
15Вот спасибо, пошел затачивать напильником под свои нужды, думал завтра опять весь день придется ковыряться с socket_select (что-то я никак не мог EOF поймать, только по таймауту отваливались, оказывается просто - сокет готов к чтению, а данных нет
), чтобы с pcntl не заморачиваться, да и не далеко везде он есть
2 Alexk: У твоего варианта один очень большой недостаток - ему слишком много нужно для работы, кроме собственно PHP (про ОС и коннект не вспоминаем
)::
)
- веб-сервер
- браузер
- JS в браузере
Этот же скрипт можно запустить прямо из командной строки, даже в DOS’е (вроде бы
VolCh
July 16th, 2008 at 10:43
16Чего-то не хочет работать код
stream_select возвращает всегда ноль (не false), причем без таймаута, хотя в массивы прописывает сокеты, пришлось использовать count($read)+count($write) в условии
PHP Version => 5.2.4-2ubuntu5.2
Bambino
July 16th, 2008 at 12:11
17Если почитать документацию по stream_select/socket_select, то насколько я помню, таймаут нужно выставлять в 0, иначе так можно подвесить систему. Данную статью можно использовать как отправную точку, хотя думаю, что должно работать. Так как я свой пример лабал на 4-м ПХП, то с socket_select все работает.
VolCh
July 16th, 2008 at 17:50
18Как раз наоборот там написано
А на практике (особенно при работе в сотни потоков и большой нагрузке по парсингу, например регулярками сложными) вообще часто рекомендуют ставить таймаут 0, а в цикле вызывать sleep() или usleep(), поскольку если данные готовятся быстрее, чем обрабатываются, то любой таймаут эквивалентен 0, ведь к моменту вызова уже есть хотя бы один сокет готовый в работе и возврат происходит сразу же. А sleep гарантирует, что часть ресурсов процессора будет отдана и другим процессам. На десктопе подбираю время задержки так, чтобы загрузка процессора была под 80% (если один основной фоновый процесс нужный) - 20% браузеру, редакторам и прочему “офисному” вполне хватает
Как отправную я и использовал, просто удивился, что с копи-паста не заработало
RSS feed for comments on this post · TrackBack URI
Оставьте комментарий