[Эту мини-статью я когда-то написал для журнала "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)
Перевод: :iam:
Оригинал статьи


Вы прочитали статью и она Вам понравилась?
Подпишитесь на обновления!

Pаспечатать эту статью Pаспечатать эту статью