Написание программ на PHP с использованием fork()

    Параллельные программы на PHP


    Раньше заголовок темы был «Написание многопоточных программ на PHP». В PHP есть ровно один «нормальный» способ писать приложения, которые используют несколько ядер/процессоров — это fork(). О прикладном использовании системного вызова fork() в языке PHP и расширения pcntl я и расскажу. В качестве примера мы напишем достаточно быструю параллельную реализацию grep (со скоростью работы, аналогичной find . -type f -print0 | xargs -0 -P $NUM_PROCS grep $EXPR).

    Реализация


    Реализация этого системного вызова в PHP очень проста:

    PHP_FUNCTION(pcntl_fork) {
    	pid_t id;
    	id = fork();
    	if (id == -1) {
    		PCNTL_G(last_error) = errno;
    		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error %d", errno);
    	}
    	RETURN_LONG((long) id);
    }
    

    Что такое системный вызов fork()


    Системный вызов fork() в *nix-системах представляет из себя такой системный вызов, который делает полную копию текущего процесса. Системный вызов fork() возвращает своё значение два раза: родитель получает PID потомка, а потомок получает 0. Как ни странно, во многих случаях только этого достаточно для того, чтобы писать приложения, использующие несколько CPU.

    $ php -r '$pid = pcntl_fork(); echo posix_getpid() . ": Fork returned $pid\n";'
    9545: Fork returned 9546
    9546: Fork returned 0
    


    Подводные камни при использовании fork()


    На самом деле fork() делает свою работу не задумываясь о том, что находится у пользовательского процесса в памяти — он копирует всё, например функции, которые зарегистрированы через atexit (register_shutdown_function). Пример:

    $ php -r 'register_shutdown_function(function() { echo "Exited!\n"; }); pcntl_fork();'
    Exited!
    Exited!
    

    К сожалению, PHP в конце выполнения скрипта осуществляет вызов деструкторов (в том числе и внутренних деструкторов ресурсов соединений с базой данных). Пример для расширения mysqli:

    <?php
    /* test.php */
    $conn = new mysqli(..., "mysql") or die("Cannot connect\n");
    $pid = pcntl_fork();
    if ($pid > 0) {
        echo "Parent exiting\n";
        exit(0);
    }
    echo "Sending query\n";
    $res = $conn->query("SHOW TABLES") or die("Cannot get query result\n");
    print_r($res->fetch_all());
    
    /*
    $ php test.php
    Parent exiting
    Sending query
    Warning: mysqli::query(): MySQL server has gone away in test.php on line 9
    Warning: mysqli::query(): Error reading result set's header in test.php on line 9
    Cannot get query result
    */
    

    Вывод программы будет не обязательно таким, как написано. Иногда потомок «успевает» до исполнения процедуры закрытия соединения в родителе и всё работает, как надо.

    Боремся с отложенным исполнением функций / деструкторов


    На самом деле, проблему с отложенным исполнением можно решить, если вы точно знаете, что хотите. Например, в Си есть функция _exit(), которая выходит, не запуская никаких установленных обработчиков. К сожалению, в PHP такой функции нет, но её поведение можно частично эмулировать с использованием сигналов:

    function _exit() {
        posix_kill(posix_getpid(), SIGTERM);
    }
    

    Этого «хака» нам будет достаточно, чтобы соединение с базой оставалось активным для двух PHP-процессов одновременно, хотя лучше, конечно, так на практике не делать :):

    <?php
    /* test.php */
    $conn = new mysqli(..., "mysql") or die("Cannot connect\n");
    function _exit() {
        posix_kill(posix_getpid(), SIGTERM);
    }
    function show_tables() {
        global $conn;
        echo "Sending query\n";
        $res = $conn->query("SHOW TABLES") or die("Cannot get query result\n");
        echo "Tables count: " . $res->num_rows . "\n";
    }
    $pid = pcntl_fork();
    
    if ($pid > 0) {
        show_tables();
        _exit();
    }
    
    sleep(1);
    show_tables();
    /*
    $ php test.php
    Sending query
    Tables count: 24
    Terminated: 15     <--- это вставляет командный интерпретатор
    $ Sending query
    Tables count: 24
    */
    

    Пишем grep


    Давайте теперь, для примера, напишем простенькую версию grep, которая будет искать по маске в текущей директории.

    <?php
    /* Пример использования:
    $ php grep.php argv
    ./grep.php:$pattern = "/$argv[1]/m";
    */
    exec("find . -type f", $files, $retval); // получаем список всех файлов в директории
    $pattern = "/$argv[1]/m";
    foreach($files as $file) {
        $fp = fopen($file, "rb");
        // файл с очень большой вероятностью является двоичным, если в нём встречаются нулевые байты
        $is_binary = strpos(fread($fp, 1024), "\0") !== false;
        fseek($fp, 0);
        if ($is_binary) {
            if (preg_match($pattern, file_get_contents($file))) echo "$file: binary matches\n";
        } else {
            while (false !== ($ln = fgets($fp))) if (preg_match($pattern, $ln)) echo "$file:$ln";
        }
        fclose($fp);
    }
    

    Пишем параллельную версию grep


    Теперь подумаем, как же можно ускорить данную программу, распараллелив её. Можно легко заметить, что мы можем разделить массив $files (список файлов) на несколько частей и обработать эти части независимо. Причём мы можем так делать во всех случаях, когда у нас есть какой-то большой список задач: просто берем каждый N-ный в соответствующем процессе и обрабатываем его. Поэтому, напишем более-менее общую функцию для этого:

    define('PROCESSES_NUM', 2); // задаем количество потоков для обработки
    function parallelForeach($arr, $func)
    {
        for ($proc_num = 0; $proc_num < PROCESSES_NUM; $proc_num++) {
            $pid = pcntl_fork();
            if ($pid == 0) break;
        }
    
        if ($pid) {
            for ($i = 0; $i < PROCESSES_NUM; $i++) pcntl_wait($status);
            return;
        }
    
        // обходим каждый PROCESSES_NUM элемент массива и обрабатываем его
        $l = count($arr);
        for ($i = $proc_num; $i < $l; $i += PROCESSES_NUM) $func($arr[$i]);
        exit(0);
    }
    


    Осталось заменить foreach() на использование нашей функции parallelForeach и добавить обработку ошибок:
    Полный исходный текст
    <?php
    /* parallel-grep.php */
    define('PROCESSES_NUM', 2);
    
    if ($argc != 2) {
        fwrite(STDERR, "Usage: $argv[0] <pattern>\n");
        exit(1);
    }
    
    grep($argv[1]);
    
    function grep($pattern)
    {
        exec("find . -type f", $files, $retval);
        if ($retval) exit($retval);
    
        $pattern = "/$pattern/m";
        if (false === preg_match($pattern, '123')) {
            fwrite(STDERR, "Incorrect regular expression\n");
            exit(1);
        }
    
        parallelForeach($files, function($f) use ($pattern) { grepFile($pattern, $f); });
        exit(0);
    }
    
    function grepFile($pattern, $file)
    {
        $fp = fopen($file, "rb");
        if (!$fp) {
            fwrite(STDERR, "Cannot read $file\n");
            return;
        }
    
        $binary = strpos(fread($fp, 1024), "\0") !== false;
        fseek($fp, 0);
        if ($binary) {
            if (preg_match($pattern, file_get_contents($file))) echo "$file: binary matches\n";
        } else {
            while (false !== ($ln = fgets($fp))) {
                if (preg_match($pattern, $ln)) echo "$file:$ln";
            }
        }
    
        fclose($fp);
    }
    
    function parallelForeach($arr, $func)
    {
        for ($proc_num = 0; $proc_num < PROCESSES_NUM; $proc_num++) {
            $pid = pcntl_fork();
            if ($pid < 0) {
                fwrite(STDERR, "Cannot fork\n");
                exit(1);
            }
            if ($pid == 0) break;
        }
    
        if ($pid) {
            for ($i = 0; $i < PROCESSES_NUM; $i++) {
                pcntl_wait($status);
                $exitcode = pcntl_wexitstatus($status);
                if ($exitcode) exit(1);
            }
            return;
        }
    
        $l = count($arr);
        for ($i = $proc_num; $i < $l; $i += PROCESSES_NUM) $func($arr[$i]);
        exit(0);
    }

    Проверим работу нашего грепа на исходном коде PHP 5.3.10:

    $ php ~/parallel-grep.php '^PHP_FUNCTION' | head
    ./ext/calendar/calendar.c:PHP_FUNCTION(cal_info)
    ./ext/calendar/calendar.c:PHP_FUNCTION(cal_days_in_month)
    ./ext/calendar/calendar.c:PHP_FUNCTION(cal_to_jd)
    ./ext/calendar/calendar.c:PHP_FUNCTION(cal_from_jd)
    ./ext/calendar/calendar.c:PHP_FUNCTION(jdtogregorian)
    ./ext/calendar/calendar.c:PHP_FUNCTION(gregoriantojd)
    ./ext/calendar/calendar.c:PHP_FUNCTION(jdtojulian)
    ./ext/calendar/calendar.c:PHP_FUNCTION(juliantojd)
    ./ext/calendar/calendar.c:PHP_FUNCTION(jdtojewish)
    ./ext/calendar/calendar.c:PHP_FUNCTION(jewishtojd)
    
    $ time php ~/parallel-grep.php '^PHP_FUNCTION' | wc -l
        4056
    
    real	0m2.073s
    user	0m3.265s
    sys	0m0.550s
    
    $ time grep -R '^PHP_FUNCTION' . | wc -l
        4056
    
    real	0m3.646s
    user	0m3.415s
    sys	0m0.209s
    
    $ time find . -type f -print0 | xargs -0 -P 2 grep '^PHP_FUNCTION' | wc -l
        4056
    
    real	0m1.895s
    user	0m3.247s
    sys	0m0.249s
    


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

    Комментарии 62

      +16
      В целом статья хороша, плюсанул. Но замечание у меня все же есть.
      ИМХО, не стоит расшаривать между несколькими процессами одно и то же соединение с БД, если вы не знаете, как именно клиентское API данной БД ведет себя в таких случаях. Можно нарваться на рассинхронизированную ситуацию, когда запросы к БД и ответы перемешаются в сокетах.
      Вообще гуайды рекомендуют непосредственно перед fork() закрывать все дескрипторы, кроме тех с которыми вы «точно знаете, что делаете», типа слушающих сокетов.
      В большинстве случаев будет правильным устанавливать в каждом клиенте свое соединение с БД после fork(). Тогда и закрытие коннекта в каждом клиенте не вызовет описанных в посте проблем.
        0
        А что по поводу обработки изображений? Вообще как можно ускорить обработку изображений? Кроме как на пользовательской стороне используя flash…
          0
          по числу ядер можно распараллелить обработку нескольких изображений…
          +7
          В заголовке обещаете многопоточную программу, в статье пишите про процессы.
            –4
            2 процесса = 2 потока. Параллелизм уровнем выше :)
              +7
              2 процесса = 2 процесса. Вы принципиальную разницу между потоками и процессами представляете?
                0
                Я даже представляю как сделать параллелизм внутри приложения на файберах (возможно раскрою вам секрет, но были операционки без «потоков» (вообще говорят нитей)).
                Вы не верите мне что в 2х процессах в сумме будет МИНИМУМ 2 нити? :)
                  +3
                  Конечно я вам не верю, с чего бы верить. Вы упорно показываете мне, что разницы не понимаете.
                    +1
                    Я вам ничего не показываю. Выводы из сказанного мною вы вольны делать сами.
                    Основная мыль неизменна.

                    2 процесса = минимум 2 нити. Как следствие параллелизм уже будет. Но уровнем выше. Если цель — достичь параллелизм в программе — это достижимо через форк.

                    Это не многопоточность в её классическом понимании (потому что как сказал Sega100500 новые нити будут в другом адресном пространстве), но тем не менее параллельное исполнение того же кода происходит.
                      +1
                      Да, это параллельность, но все же это не потоки, а параллельные процессы, хотя бы с этим согласитесь.
                        +2
                        Сказать, что «2 процесса = минимум 2 нити» это как сказать «программируя на Си, я программирую на ассемблере» — а чего, всё равно же в результате будут те же самые машинные коды.
                        Это не многопоточность в её классическом понимании… но тем не менее параллельное исполнение того же кода происходит.
                        Это вообще не многопоточность, в любом понимании. И, конечно, параллельное выполнение происходит, потому что многопоточность — не единственный способ выполнить что-то параллельно.
                          –3
                          Ассемблер — это язык. И программируя на C, вы не программируете на ассемблере. Компилятор С компилирует в бинарник, в машинные инструкции. Пользуясь вашим подходом скажу что вы не понимаете разницу между языком ассемблер и машинными инструкциями :)
                            +2
                            Вы точно прочитали то, что я вам написал, нет? Прочтите первое предложение от начала и до конца и обратите внимание на слова «это как сказать».
                              0
                              Но дело в том, что система создавая процесс порождает в нем главный поток.
                              В 2х процессах будут минимум 2 потока.

                              Вы говорили, что не согласны с этим утверждением. Уточните с чем именно.
                                +1
                                Я в первом же комментарии написал с чем несогласен. В статье — не многопоточная программа.
                                  0
                                  Не надоело еще в каждой статье про распараллеливание в PHP доказывать что это нельзя называть многопоточностью? Многопроцессность звучит уж слишком коряво. Никто не подразумевает реальную многопоточность. Только эмуляцию.
                                  Используя форки в php обычно пытаются эмулировать именно многопоточность, поэтому такое название. Если вы вообще программируете на PHP, то в его контексте этот термин должен быть вполне понятен.
                                    +9
                                    Я против бардака.

                                    Во-первых, если многопроцессность плохо звучит, никто не мешает выдумать новый термин. Например, можно писать «эмуляция многопоточности».

                                    Во-вторых, в ПХП многопоточность может появиться, что тогда делать с горой мусора.

                                    В-третьих, в программировании термины важны, чтобы понимать о чём вообще речь, а не расписывать абзацем что вы имели ввиду.
                                      0
                                      Предложите свое название статьи, я поправлю. Заметьте, что в тексте я термин «многопоточность» не использовал :), название в заголовке я использовал для краткости.
                                        0
                                        «Написание многозадачных программ на PHP»
                                          0
                                          Да уж…
                                            +1
                                            Поменял название. Надеюсь, так лучше и больше не вызывает у «понимающих людей» такого страшного баттхёрта :).
                                        +3
                                        А что, давайте называть компиляцию интерпретацией, процедурное программирование — функциональным, а машину, выполняющую байт-код — микропроцессором?
                                          0
                                          Паралелизм в программах на PHP.

                                          По феншую это не потоки же всетаки )
                                            +1
                                            Процедурное программирование функциональным уже многие называют :( Наверное потому что в Си нет отдельного понятия процедуры, а просто void функция. Прикольно бывает слышать из уст «крутого» java-кодера: «php отстой, все быдлокодеры пишут на нём в функционально, а не ооп».
                                              +1
                                              Я скажу честно: я не знаю, как называется такой стиль программирования, когда использует только fork() + примитивы IPC, но не разделяемая память. Термин «многопоточность» здесь не очень подходит, но остальные — ещё хуже, ибо они не отражают возможностей и особенностей fork().
                                          –1
                                          С этим утверждением я не спорил. Я вам написал что 2 процесса = 2 потока.
                                          Этакая «квазимногопоточность». Ещё раз. Я согласен что в статье нет «многопоточности».

                                          Кроме того что вы не заметили смайлик в моём сообщении, вы ещё и зачем то зацепили меня намекая на отсутствие у меня знаний в области параллелизма.

                                          НЕ нужно так остро реагировать :)
                                            +5
                                            А я вам написал, что ваше утверждение равнозначно утверждению, что «программируя на Си, я программирую на ассемблере». Вы ещё скажите, что если рядом поставить две машины, где выполняются по одному процессу, то получится многопоточная среда. Разницы между этим и вашем утверждением я не вижу.

                                            Термин «многопоточность» говорит о множестве потоков в одном процессе. Так что 2 процесса ≠ 2 потока. 2 процесса = 2 процесса.
                                              –1
                                              Вы не прочитали сообщение на которое ответили :)
                        +1
                        Напомнило мне вот такую историю: ithappens.ru/story/358
                        +4
                        Ожидал про какой-нибудь libevent прочитать, а опять дурацкими везде описанными процессами кормят.
                        Заголовок врет (сейчас он «Написание многопоточных программ на PHP»).
                        +8
                        Если мне память не изменяет, то многопоточность предполагает использование единого адресного пространства одного процесса. В данном же случае, если необходимо взаимодействие между такими «потоками», потребуется организовывать межпроцессное взаимодействие.
                          0
                          Удобное межпроцессное взаимодействие организовать в принципе не трудно. Чуть выше написал ссылку на статью, где организовано полноценное взаимодействие процессов с использованием fork и libevent.
                          Реализована передача данных в обе стороны, включая события. Это позволяет еще и использовать один процесс многократно не тратя ресурсов на новый форк.
                            0
                            Дело ведь не только в межпроцессном взаимодействии, а еще и в синхронизации общих данных между такими вот параллельными «потоками». Например, обрабатывая один массив параллельно, сделать так, чтобы он выглядел всегда одинаково для каждого из таких «потоков».
                          +9
                          2012 год, вы пишете на хабре как использовать fork в php
                          фуфу?
                            0
                            Согласен. Но около 200 добавлений в избранное свидетельствуют о том, что еще не все на хабре разбираются в том, как работает этот системный вызов. Более того, я ни разу не утверждаю, что я сам знаю о всех тонкостях работы с ним, и сам с радостью бы почитал такого рода статейки, если бы они были.
                              0
                              Новичков всегда хватает. Только все эти статьи написаны же уже.
                                0
                                И да и нет. К сожалению, всё, что я видел, касалось демонов на PHP, или долго исполняющихся программ. Моя заметка касается программ, которые исполняются несколько секунд и работают в интерактивном режиме с точки зрения пользователя.
                                0
                                Вы вводите новичков в заблуждение, а опытных раздражаете термином многопоточность.
                                  +1
                                  Убрал термин «многопоточность» из заголовка, чтобы не смущать «опытных».
                              0
                              Статья не плохая, но тут много лишних «наворотов»… а каждый такой «наворот» может привести к потенциальной ошибки.
                              Первое, что хотелось отметить: РНР — это не тот язык, где должна использоваться параллельная обработка. надо просто принять это, а не правой рукой чесать левое ухо.
                              Второе и главное замечание: если хотите делать форк, то делайте его до открытия всяких файлов, сокетов и соединений с БД (хотя это практически одно и тоже)
                              Как упоминалось в статье — pcntl_fork() — это всего лишь враппер на системным выводом fork
                              для справки читаем man fork и у вас сразу предотвратится куча багов
                                0
                                Статья ценная, решает много вопросов, связанных с адекватной скоростью работы скриптов больших PHP-сайтов. Единственный вопрос: как вы решаете/предлагаете решать вопрос передачи данных между форками? Хотя бы на уровне возвращения значений из дочерних форков.
                                  0
                                  Лично я бы если бы и решал это на пхп, то смотрел исключительно в эту сторону blog.kamisama.me/2011/11/07/php-rescue-with-phpredis/
                                    0
                                    Конечно, зачем нам нативные пайпы, давайте лучше поставим еще один сервак с редисом.
                                      0
                                      семафоры не решают всей проблемы
                                    0
                                    Самый простой способ — через временные файлы и файловые же блокировки… Есть более продвинутые способы межпроцессного взаимодействия, но суть не отличается: весь обмен данными только в сериализованном виде.
                                      0
                                      обмен через shmem — более продвинуто :)
                                    0
                                    А как оно будет работать из mod_php например? Форкать весь апач?
                                      0
                                      ru2.php.net/manual/en/pcntl.installation.php

                                      Process Control support in PHP is not enabled by default. You have to compile the CGI or CLI version of PHP with --enable-pcntl configuration option when compiling PHP to enable Process Control support.
                                        0
                                        Впрочем, люди пишут, что используют. Но нужно понимать, что форкать апач — это очень плохая идея. То же самое касается PHP-FastCGI / PHP-FPM. То есть, нужно очень хорошо понимать, что вы делаете, и зачем. Тот же system() на самом деле форкается, но он это делает очень аккуратно, причём он делает именно fork()-exec(), а не просто fork(). Лучше не думайте о том, чтобы использовать fork() на стороне mod_php или FPM.
                                        0
                                        Такие штуки нужны для скриптов, а там ни о каком mod_php речи не идёт. В обычной выдаче такие вещи лучше решать другими путями — например, через fastcgi_finish_request()
                                          0
                                          >А как оно будет работать из mod_php например? Форкать весь апач?
                                          НИ КАК
                                          читаем внимательно ман…
                                          pcntl_fork() не работает в mod_php
                                            0
                                            Делать нечего, только читать маны по языкам на которых я не пишу.
                                              0
                                              это не ман по языку, а ман по системным вызовам, которые ты, как программист, должен знать, вне зависимости от языка, на котором идет разработка.
                                                0
                                                Казалось бы, при чем здесь «вне зависимости от языка» и «mod_php».
                                          0
                                          Я уже упоминал, что PHP не предназначен для параллельной обработки данных, в том виде, ка мы ее понимаем :)

                                          Единственное и правильное применение fork() в PHP — это ввод процесса в бэдграунд или демонизация процесса.

                                          Вся параллельная обработка данных решается на уровне архитектуры между процессами.
                                          Хотим организовать параллельную обработку каких-то данных — запускаем (форкаем один раз) сколько надо процессов, обмен с которыми идет либо по shmop, либо по сокетам или shmem.
                                          мастер процесс может посылать сигналы (USR1, USR2 ) на начало обработки и принимать ответные сигналы (правда он не будет знать, от какого процесса пришел этот сигнал)
                                          можно/нужно использовать симофоры для синхронизации окончания обработки данных процесом.

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

                                          Архитектура приблизительно должна быть следующей: WEB скрипт через сокеты или shm закладывает некоторые данные, посылает сигнал мастер процессу и заканчивается.
                                          Мастер процесс тиражирует этот сигнал по дочерним процессам, задача начинает решаться…

                                          WEB скрипт по AJAX через некоторые промежутки тягает WEB скрипт, который просматривает статус задачи (В ПРОЦЕССЕ), который находится в определенном блоке shm

                                          Как только все процессы завершат исполнение задачи и перейдут в режим ожидания новой, мастер процесс получит уведомление, сформирует окончательные данные и обновит блок статуса задачи на ВЫПОЛНЕНО.

                                          WEB скрипт по AJAX через WEB скрипт, просмотрит статус задачи (ВЫПОЛНЕНО) и вызовит скрипт показа результатов.

                                          как-то так…
                                            0
                                            И никаких форков во время выполнения WEB скриптов!!!
                                              0
                                              а пока Пользователь ждет расчета своей мега-задачи ему пожно показывать
                                              красивый прогрессбар или крутить песочные часики…
                                                0
                                                Ну, да, всё правильно, кроме того, что эти скрипты могут исполняться не на той же машине, поэтому синхронизацию лучше делать не через shm, а, например, через базу данных.
                                                  0
                                                  ну тогда уж через сокеты. по этому я использую libevent и для сигналов и для обмена данными с фронт-скриптами. А для воркеров я бы предпочел обмен через shm.
                                                  База — это слишком тяжело, и лишнее звено. Базу надо использовать для хранения данных, а не для обмена.
                                                  несомненно, лучше все тяжелые вычисления запускать на другой машине, если она конечно есть.

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                              Самое читаемое