Параллельные вычисления, класс-обёртка для pcntl_fork()

    Хочу показать свой базовый класс, который я использую для PHP скриптов.
    Приемущество его в том, что легко можно «распараллелить» работу.
    Используется pcntl_fork() со всеми «вытекающими».

    (тестировалось только на линукс)

    Суть идеи:



    class some_script extends CliScript
    {
      protected function processWorker($item)
      {
          $this->log("I'm doing heavy job");
          sleep(rand(1,5));
          $this->log("I'm done doing heavy job");
      }

    }

    $script = new some_script();
    $script->setWorkers(5);
    $script->run();


    в итоге получим один родительский процесс и 5 дочерних делающих «heavy job».

    Есть свои нюансы и ограничения: открытые соединения с базой и файлы могут свести сума.

    Нужен другой подход: родительский процесс должен заниматься базой и дочерние процессы делать «черную работу» и возвращать результат. Вот например:

    class master_and_workers extends CliScript
    {
        protected $contracts = array(2,4,5,1,3,7,3,1,4,9,2,4,1);
        protected $results;

        protected function processMaster()
        {
        foreach($this->contracts as $contract)
        {
          while!$this->canStartWorker() ) { sleep(1); };

          $this->startWorker($contract);
        }

        $this->waitForChildren();

        var_export($this->results);
        }

        protected function processWorker($item)
        {
        $this->log("I'm busy for {$item} seconds...");
        sleep($item);
        $this->log("Job is done.");

        return "Job is done. Sleep time was {$item}";
        }

        protected function processResult($result)
        {
        $this->results[] = $result;
        }



    $script = new master_and_workers();
    $script->setWorkers(3);
    $script->run();


    Быстрое пояснение, если кто запутался:

    после старта в родительском процессе выполняется метод processMaster(), который стартует дочерние процессы.
    В дочернем процессе выполняется метод processWorker().
    То, что дочерний процесс возвращает — сохраняется во временном файле, после завершения вызывается метод processResult() в родителе и передается туда результат.

    в классе CliScript есть несколько полезных методов:

    getRunningTime() возвращает время выполнения от старта в секундах
    countWorkers() возвращает количество дочерних процессов (имеет смысл только в родтельском процессе)
    log() если задан файл CliScript::$file_log то логируется, если нет — на экран. Выводится информация о том, «кто сообщает»

    Чтобы убить всю компанию «нежно» отправте сигнал SIGTERM родительскому процессу, там есть примитивный обработчик.

    В завершение.



    Не могу сказать что код обкатанный и вылизаный, скорее наоборот, но мне он очень помогает быстренько загрузить CPU работой когда надо.

    исходник класса CliScript

    Если у кого-то есть похожие наработки и готов поделится — буду очень рад.
    Поделиться публикацией

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

      0
      не совсем понятно как ваш метод будет работать с транзакциями, когда нужно параллельно обрабатывать несколько потоков данных и каждый в своей транзакции, если у вас только один коннект к базе в родительском процессе. Т.е. подразумевается что любой поток может упасть и откатить транзакцию без вреда для остальных
        +17
        bcgjkmpjdfnm
          +5
          лаконично
          +1
          Блин, простите.
          Так вот, использовать родительские подключения к базе нельзя. Как только любой из детей умрет, с ним умрет и коннект.

          Поэтому если база нужна и в родителе и в потомках — нельзя допускать чтобы соединение было открыто на момент вызова pcntl_fork().
            0
            Быть может я не совсем понимаю, но вроде же есть pconnect, который живет дольше (по крайней мере в частном, но самом частом случае СУБД — mysql).
              0
              Я честно признаюсь, что не до конца знаю внутренности работы с pconnect, но у меня не заработало ни так ни так. Первый запрос делается, второй возвращает ошибку. Как мне кажется, проблема в том, что у коннекта есть некий уникальный идентификатор, и даже если коннект остается открытым на уровне расширения — экземпляр обертки в PHP уничтожается, тем самым отвязывая идентификатор от коннекта. При повторной попытке обратиться с тем же идентификатором начинается печалька.
                0
                pconnect связан с транзакциями как опера с балетом
                  0
                  Вы, наверно, что-то перепутали, я ничего не говорил о транзакциях. Речь идет о подключении к базе и его области и времени жизни.
                0
                Вот именно, что нельзя юзать родительский коннект, и вы решили не побороть проблему, а обойти ее путем перекладывания всей работы с базой в родителя

                >> родительский процесс должен заниматься базой и дочерние процессы делать «черную работу» и возвращать результат

                т.о. получаются следующие ограничения
                во-первых, потомки не могут полноценно работать с базой (транзакции и все такое)
                во-вторых, и это очень важно, перекладывая работу с базой на один поток, мы лишаемся всех преимуществ многопоточной работы, ведь согласитесь, 16 потоков на 16 процах писали бы в базу гораздо быстрее чем один
                в-третьих, когда все потомки отработали и передали данные в родителя для записи в базу, ему нужно все эти данные записать по-очереди, и пока он пишет, он не может запустить еще потомков, ну либо тут нужно организовывать какую-то асинхронную очередь для их обработки

                в общем сплошные минусы, а решение то на самом деле элементарное — сразу после форка сбрасывать коннект и соединяться заново, а родителю перед его запросами проверять — не разорван ли его коннект и пересоединяться если нужно
                  0
                  Мне кажется вы хотели ответить Автору. Лично я вообще работаю с базой только из потомков, поскольку зачастую обращение к удаленному серверу БД является наибольшей задержкой, и распараллеливание запросов к разным серверам по форкам дает существенный выигрыш.
                    0
                    нельзя всем потокам использовать одно подключение к базе, потому что когда будет race condition по использованию одного подключения из 50 потоков база скажет «простите, но я пошла нахрен» и просто закроет подключение. уже сталкивался с таким.

                    лучшим решением тут будет из каждого чайлда создавать собственное подключение к базе, у родителя свое
                      0
                      ну в общем то, о чем я и говорил
                    0
                    В случае данного CliScript коннект с базой данных существовать будет даже после «смерти» детей, т.к. там вызывается
                    system("kill -9 {$this->pid}");
                    он не освобождает ресурсы.
                      0
                      Из опыта скажу что mysqli разрывает соединение как у детей так и родителя, нужен реконнект.
                      0
                      Как вариант, использовать такую штуку Asynchronous Tasks
                      0
                      (тестировалось только на линукс)

                      Отчего же на винде не потестили? Note: This extension is not available on Windows platforms. Документация — очень интересная штука, между прочим.
                        +5
                        [offtop]Неужели кто то поднимает на винде сервера с php?[/offtop]

                        Не понятна отсылка к докам.
                        Автору видимо это не нужно, а кому нужно будет, сами почитают, разве не очевидно?
                        +1
                        code.google.com/p/scalr/source/browse/scalr-2/trunk/app/cron-ng/jobs/Poller.php

                        Посмотрите как работают наши кронджобы. Мы используем Semaphore Functions: очереди, семафоры и шаред мемори. Все настраивается, можно к примеру запустить пулл процессов и он будет висеть и обрабатывать задачи как толкьо они будут появлятся в очереди. Можно просто переодически запускать крон, он стартует воркеры, делает работу и прибивается. И много других вкусных плюшек.
                          +2
                          У меня есть некоторые наработки в эту сторону github.com/kulikov/php-threads-manager

                          там мультипроцессорность реализуется тремя способами (на выбор):
                          1. через запуск отделных процессов через popen()
                          2. через tcp сокеты stream_socket_client()
                          3. через pcntl_fork() // не доделано

                          В основном я пользовался popen как самым надежным и прозрачным способом. сокеты использовлись коллегами для дебага под виндой. а форки так и не доделал.
                          Самый главный вопрос: как наиболее оптимально организовать обмен данными между родителем и потомком. По завершению работы в потомке складывать ответ во временный файл, а родитель из него потом все считывает? Пожалуй надо так и сделать. :)
                            0
                            я тоже делал через popen
                            работает стабильно
                              0
                              Что-то мне кажется, что shm_* (или shmop_* — но их не юзал) должны быть более эффективны, особенно учитывая нативные реализации семафоров и очередей (на которых довольно просто реализуется, например пул соединений с СУБД).
                                0
                                да, согласен. шаред мемори должно быть быстрее. надо попробовать прикрутить.
                                  0
                                  шаред мемори однозначно быстрее
                                  0
                                  не делал не говори,
                                  реализовать пул соединений с БД не так уж и просто.
                                    0
                                    имеется ввиду для параллельного использования.
                                  0
                                  graber, переделай в своем проекте комменты на английский.
                                  По своему опыту скажу. что очень тяжело использовать чужой код с комментами на японском или испанском.
                                  +2
                                  Всё это проходил и из опыта вот что могу сказать:
                                  Если подключить PECL-расширение proctitle (расширение бета, по причине отсутствия тестов, но работает исправно, проверено) то воркеры смогут задавать свои заголовки в ps или top, например. Это полезно что бы знать кто какие роли выполняет и чем занят.
                                  Fork работает по принципу copy-on-write поэтому самое тяжелое лучше выгрузить в память сразу.
                                  Некоторые расширения ведут себя не корректно после fork-а. Так, например, mysqli разрывает соединение не только в чаилдах, но и в родителе.
                                  Комуникацию по коммандам удобно длеать через сигналы через расширение POSIX. Для более сложной комуникации лучше использовать pair сокеты. По pair советам можно отдавать результат из воркера в мастер по завершению воркера (как-то проще чем через семафоры/мутексы). При включении в дело libevent можно получить очень функциональный событийный демон.
                                  Не забывайте делать setsid (posix_setsid).
                                    0
                                    Спасибо! Это то, чего мне не хватает как раз. Сходу непонятно про session leader. Если сможете на пальцах объяснить что к чему то спасибо :)
                                      +1
                                      Если вы про SID то setsid запускает новую сессию для вызвавшего setsid процесса причем SID равен PID-у этого процесса (этот процесс называется лидером). Таким образом процесс становится независим от чужой сессии, которая может быть кем-то прибита. Каждый последующий порожденный процесс от лидера или его потомка будет содержатся в этой сессии. В некоторых системах при падении лидера падает и сессия (но жаль что далеко не во всех). По сути, сессия позволяет связать пачку процессов: команада pkill -s SID разошлёт сигнал всем процессам в сессии. ps -Fs SID выведет процессы сессии и т.д.
                                    +2
                                    писали игровых демонов на php. pcntl+libevent. демон висел на порту и слушал и обрабатывал HTTP запросы. был некой прослойкой между фронтом и БД. запускалось форками. управление через SIGNAL. Libevent позволял создавать таймеры и прочую вкусноту. в процессе разработки были исправлены баги в php_libevent с Тони.
                                      0
                                      Андрей,
                                      просто автор немного не в теме оборотной стороны медали fork()
                                        0
                                        А сколько ещё надо пофиксить… :)
                                        0
                                        Понравилось :)

                                        case SIGTERM:
                                            echo 'opa!';
                                            return;
                                          0
                                          Автор забыл указать
                                          что это возможно только в бэдграундовских процессах
                                            0
                                            «бэдграундовских» — оговорочка по Фрейду :)
                                              0
                                              а по делу ляпнуть слабо?
                                            +1
                                            Приветствую всех)

                                            При всех моих тёплых чувствах к PHP, он, как мне кажется, не лучший инструмент для реализации многопоточности и мультипроцессности…

                                            Кроме того, как тут уже говорили, при создании многопоточного приложения всегда следует контролировать доступ к общим ресурсам… Для примера в методе log (к которому насколько я понимаю обращаются «рабочие») следовало воспользоваться хотя бы функцией file_put_contents с флагами FILE_APPEND и LOCK_EX.

                                            Ещё бросилось в глаза в методе getFormattedMessage почему-то автор предпочитает вызвать 4 раза функцию str_replace:
                                            $message_formatted = str_replace('%time', $time, self::OUTPUT_FORMAT );
                                            $message_formatted = str_replace('%role', $role, $message_formatted);
                                            $message_formatted = str_replace('%pid', $this->pid, $message_formatted);
                                            $message_formatted = str_replace('%message', $message, $message_formatted);
                                            но ведь можно было использовать массивы в качестве параметров $search и $replace… хотя тут вообще уместна другая функция — sprintf (или её аналог vsprintf)…

                                            Код просматривал мельком, поэтому сказал о том, что бросилось в глаза (getFormattedMessage рядом с методом log =) ).

                                            Многопоточность и мультипроцессность — эта та область где даже «гуру» совершают ошибки… возможно стоит получше изучить сам php, прежде чем хвататься за многопоточность. А если уж берёшься за неё, то лучше хорошенько разобраться во всех механизмах прежде, чем писать на хабр.
                                              0
                                              Ваша активность на хабре многословна. Сразу видно, что Вы очень хорошо в чем-то разбираетесь и хочется прислушаться к Вашим советам сильно-сильно! Вы случайно не преподаватель программирования?
                                                0
                                                я не считаю уровень своих знаний достаточным, чтобы писать статьи на хабр.
                                              0
                                              Может быть удобнее будет запускать процесс с помощью system(«php /path/script.php»);?
                                              Честно говоря, не проверял этот метод, но управляю демонами через PHP
                                              #!/usr/bin/php
                                              <?php
                                              echo "________________________________________\n";
                                              $command = "restart";
                                              if (isset($argv[1]))
                                                  $command = trim($argv[1]);
                                              echo "Начинаем сканировать процессы...\n";
                                              exec("ps -A -F | grep php", $output);
                                              $folder = dirname(__FILE__);
                                              $php = "php";
                                              $names = array(
                                                  "system",
                                                  "inspector",
                                                  "drivers",
                                                  "android",
                                              );
                                              echo "Определяем демоны multitaxi...\n";
                                              foreach ($output as $out) {
                                                  if (preg_match_all("#.*php ".$folder."/(.*)\.php#isU", $out, $matches)) {
                                                      $matches[0][0] = str_replace("  ", " ", $matches[0][0]);
                                                      $matches[0][0] = str_replace("  ", " ", $matches[0][0]);
                                                      $matches[0][0] = str_replace("  ", " ", $matches[0][0]);
                                                      $matches[0][0] = str_replace("  ", " ", $matches[0][0]);
                                                      $matches[0][0] = str_replace("  ", " ", $matches[0][0]);
                                                      $daemon = $matches[1][0];
                                                      $matches = explode(" ", $matches[0][0]);
                                                      $pid = $matches[1];
                                                      //echo $daemon;
                                                      if (in_array($daemon, $names)) {
                                                          if ($command != "view")
                                                              exec("kill -9 ".$pid);
                                                          echo "Процесс: ".$daemon." (".$pid.")\n";
                                                      }
                                                  }
                                              }
                                              if ($command != "stop" && $command != "view") {
                                                  echo "Запускаем процессы...\n";
                                                  foreach ($names as $d) {
                                                      exec($php." ".$folder."/".$d.".php 1>> /dev/null 2>> /dev/null &");
                                                      echo $php." ".$folder."/".$d.".php 1>> /dev/null 2>> /dev/null & \n";
                                                  }
                                              }
                                              echo "Готово!\n";
                                              echo "________________________________________\n";
                                              
                                              ?>
                                              
                                              
                                                0
                                                Код писался на коленке за 5 минут. За
                                                $matches[0][0] = str_replace("  ", " ", $matches[0][0]);
                                                стыдно :)
                                                  +1
                                                  > exec($php." ".$folder."/".$d.".php 1>> /dev/null 2>> /dev/null &");
                                                  данная строка порождает Zombie
                                                  Осторожно, не наплодите стадо вампиров
                                                    0
                                                    Скрипт и написан для того, что бы не запускать стадо вампиров =) Сначала он ищет все запущенные с именами, заданными в массиве, убивает их, а потом запускает новые
                                                      –1
                                                      ну сказано же чуть ли не открытым текстом: что в данной строчке кода содержится содержится потенциальная опасность народить кучу зомби процессов, а он за свое… мой скрипт, мой скрипт…
                                                      Конечно, если интенсивность их порождения не большая, то в принципе нет ничего страшного, но еще раз повторяю, что данный код не корректен, потому-что порождение нового процесса в бэкграундовском режиме заканчивается потерей родителя (родитель заканчивается раньше, чем его потомок) — это и приводит к зомбированию. Чтоб этого не произошло надо выполнить команду setsid() — которая в реализации данного режима не предусмотрена.
                                                      для того, чтоб запускать РНР в бэкграунде и не было побочных эффектов, специально придуман php-forker
                                                        0
                                                        — Зачем Вы доите лошадь? Для этого есть корова.
                                                        — Мы не доим лошадь, мы ездим на ней
                                                        — Говорю же, лошадь не для того, что бы ее доить, а вы все моя лошадь, моя лошадь…
                                                          0
                                                          А если конкретно, этот скрипт запускает несколько зомби процессов и завершает работу. Я не вижу тут демона, родитель и должен теряться. Данный код корректен для поставленной задачи.
                                                  0
                                                  Да, есть еще замечательная штука gaerman. Можно запустить кучу одинаковых процессов, а с помощью gaerman посылать задачи и ловить ответы.

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

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