Почти настоящая многопоточность средствами php 5

    В очередной раз читал про многопоточность в php точнее полное её отсутствие и всевозможные костыли в виде не блокируемых сокетов. Вот как раз там наткнулся на интересную статью в которой описывался очень простой и эффективный способ распараллеливания потоков. На основе этого материала написал небольшой класс что бы облегчить себе работу в будущем.

    Итак код класса threads.php:
    <?php

    class Threads {
        public $phpPath = 'php';
        
        private $lastId = 0;
        private $descriptorSpec = array(
            0 => array('pipe''r'),
            1 => array('pipe''w')
        );
        private $handles = array();
        private $streams = array();
        private $results = array();
        private $pipes = array();
        private $timeout = 10;
        
        public function newThread($filename$params=array()) {
            if (!file_exists($filename)) {
                throw new ThreadsException('FILE_NOT_FOUND');
            }
            
            $params = addcslashes(serialize($params), '"');
            $command = $this->phpPath.' -q '.$filename.' --params "'.$params.'"';
            ++$this->lastId;
            
            $this->handles[$this->lastId= proc_open($command$this->descriptorSpec$pipes);
            $this->streams[$this->lastId= $pipes[1];
            $this->pipes[$this->lastId= $pipes;
            
            return $this->lastId;
        }
        
        public function iteration() {
            if (!count($this->streams)) {
                return false;
            }
            $read = $this->streams;
            stream_select($read$write=null$except=null$this->timeout);
            /* 
                Здесь береться только один поток для удобства обработки 
                на самом деле в массиве $read их зачастую несколько
            */

            $stream = current($read);
            $id = array_search($stream$this->streams);
            $result = stream_get_contents($this->pipes[$id][1]);
            if (feof($stream)) {
                fclose($this->pipes[$id][0]);
                fclose($this->pipes[$id][1]);
                proc_close($this->handles[$id]);
                unset($this->handles[$id]);
                unset($this->streams[$id]);
                unset($this->pipes[$id]);
            }
            return $result;
        }
        
        /*
            Статичный метод для получения параметров из 
            параметров командной строки
        */

        public static function getParams() {
            foreach ($_SERVER['argv'as $key => $argv) {
                if ($argv == '--params' && isset($_SERVER['argv'][$key + 1])) {
                    return unserialize($_SERVER['argv'][$key + 1]);
                }
            }
            return false;
        }
        
    }

    class ThreadsException extends Exception {
    }

    ?>


    Теперь для примера создадим test.php:
    <?php
    $start = microtime(true);

    require './threads.php';

    $threads = new Threads;

    for ($i=0;$i<10;$i++) {
        $threads->newThread('./delay.php'array('delay' => rand(15)));
    }

    while (false !== ($result = $threads->iteration())) {
        if (!empty($result)) {
            echo $result."\r\n";
        }
    }

    $end = microtime(true);
    echo "Execution time ".round($end - $start2)."\r\n";

    ?>


    И delay.php который он вызывает:
    <?php

    require './threads.php';

    if ($params = Threads::getParams()) {
        sleep($params['delay']);
        echo 'Wait for '.$params['delay'].' s.';
    }

    ?>


    В результате выполнения test.php получаем следующее:
    Microsoft Windows [Version 6.1.7100]
    Copyright (c) 2009 Microsoft Corporation. All rights reserved.

    Z:\home\labs\www\threads>php test.php
    Wait for 1 s.
    Wait for 5 s.
    Wait for 4 s.
    Wait for 5 s.
    Wait for 5 s.
    Wait for 5 s.
    Wait for 3 s.
    Wait for 3 s.
    Wait for 2 s.
    Wait for 4 s.
    Execution time 5.58

    Z:\home\labs\www\threads>

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      –1
      Делал такой через http, раздача данных делалась через POST параметры, и овтет парсился :))
      Убого, но хостинг запрещал выполнение бинарей :)
        +1
        Поверьте проще сменить хостинг, через http очень удобно когда надо загрузить кучу страниц с чужого сервера :)
          +1
          Зато с таким подходом есть потенциальная возможность распределить задание между разными серверами :)
          +2
          В curl можно делать полноценные многопаточные запросы.
          webiteam.ru/2009/04/multi-http-zapros-na-curl/
        • НЛО прилетело и опубликовало эту надпись здесь
          • НЛО прилетело и опубликовало эту надпись здесь
              –2
              почему бы и нет, если нужно обработать большое количество данных, и есть возможнсть разбить на части… это конечно не потоки, но позволяет разбить действие на чати, скорость увеличится
              +1
              Ну вот начинается… Я не плохо могу написать тоже самое на java или даже на C++ но выигрыш в скорости будет незначительный по сравнению со временем которое я потрачу на разработку и внедрение.
                +8
                На Хабре так принято не обращайте внимания)
                  –12
                  class SomeClass extends Thread {
                  public void run() {
                  // Some usefull work
                  }
                  }

                  Насколько больше времени вы потратите, написав это на Java, а не на похапе? По сути, четыре строчки на джаве против примерно тридцати на похапе. Конечно, когда ресурсы ограничены такой вариант очень даже подходит. Сам юзаю нечто подобное (только без ООП и прочих «канделябров») на шаред-хостинге. Но если вдруг состоится переезд хотя бы на VPS этот шлак обязательно заменю на джава-реализацию. Ибо велосипеды с квадратными колёсами сейчас уже мне не нравятся :)
                    +1
                    Я же специально написал на разработку и внедрение. Если уж на то пошло то я это планирую использовать на выделенном сервере для проектов pr-cy.ru.
                      –1
                      Если уже есть выделенный сервер, то к чему вопрос про проблемы внедрения? ;)

                      0
                      Thread.new do
                        код потока
                      end

                      На Ruby лаконичнее:)
                        0
                        [облизывается]
                  +6
                  Простите уважаемый автор, но то о чём вы пишите это не почти многопоточность, а то на что ссылаетесь довольно далёкое имеет отношение к неблокирующим сокетам. У вас речь идёт о небольшом классе для запуска кучи процессов CLI PHP, что уже само по себе является костылём против PCNTL, коих на Хабре не развиваемых и не поддерживаемых уже с десяток было.
                    0
                    А вы не посоветуете реализацию задачи по получению большого количества URL с последующей обработкой их содержимого?
                      0
                      Если последующая обработка не требует параллельности — то мульти-cURL вполне подойдёт. Иначе делаете аналогично автору поста — запускайте X «потоков» каждый из которых будет запрашивать и обрабатывать по одному урлу
                    0
                    А зачем нужны эти треды? Хочу пример реальной задачи :)
                      0
                      Как правило это параллельное выполнение некоторого кол-ва медленных, но не слишком ресурсоёмких задач — сетевые конекты к долгоотвечающим сервисам например
                        +2
                        А зачем их выполнять параллельно, если это не ресурсоёмкие задачи? неблокирующие сокеты + мультиплексирование достаточно для решения подобной задачи.
                          0
                          Прошу прощения, дал ответ ниже по дереву комментов. Тут главное не путать ресурсоёмкие и долго выполняющиеся. Такая простая параллельная загрузка годиться для простых, в силу каких-либо обстоятельств медленных действий, которые неудобно или невозможно параллелизировать встроенными методами PHP. В примере предлагалось запускать параллельно несколько инстансов PHP в для обхода медлительности функции mail, неисправимой медлительности могут быть особенно подвержены функции системных вызовов, комплдексные функции прочтения файлов и работы с сетью, любые сторонние библиотеки, в коде которых копаться с целью ускорения нет возможности.
                        0
                        Скачать и распарсить 50 (каждый раз разных) html-файлов, используя DOMXpath :) Делать это раз в 10 минут :) Есть шанс, что однопоточное приложение не справится за отведённый промежуток времени (10 минут), поэтому распараллеливаем выполнение — тогда общее время выполнения уменьшается до времени выполнения самого медленного потока + небольшой оверхед. Если не распараллеливать общее время выполнения будет примерно равно сумме времён выполнения задач.
                          0
                          >Если не распараллеливать общее время выполнения будет примерно равно сумме времён выполнения задач.
                          У вас всё упирается в вычислительные мощности? Concurrency недостаточно и нужен parallelism? Что там у вас за html'ки которые обрабатываются по 12 секунд. Да и если уж всё упирается в вычислительные мощности, то было бы логичнее запускать треды по кол-ву логических процессоров и передавать через тот же пайп хтмльки на обработку.
                          0
                          для выполнения ping для большого количества оборудования, например. Можно прикрутить дополнительный perl скрипт, но удобнее все-же полностью на php писать.
                            +1
                            Классический пример потоков(правда десктопный), это GUI и обработка в разных потоках. Если они будут в одном потоке, то во время обработки GUI будет подвисать.

                            Применительно к вебу — тоже самое, есть сервер, одна из функций, например, массовая рассылка почты. Появилась ситуация когда нужно разослать письма — создаем поток и рассылаем(или, если писем много, создаем n потоков). Так же очень удобно использовать потоки в грабберах. Граббинг в n потоков почти в n раз быстрее:)(до определенного момента). Хотя, в случае граббинга можно использовать curl-multi, но это если граббинг без процессинга.

                            У меня например на сервере в паралельном потоке весит джаббер бот, который меня извещает об ошибках, и имеет простенькую обработку комманд для получения статистики.

                            Конечно никто не спорит, что тоже самое можно сделать форками, либо вобще несколькими независимыми приложениями сообщающимися посредством, например, сокетов. Но используя потоки, все вразы проще:)
                            Конечно за простоту приходится платить проблемами с синхронизацией, но, при аккуратном использование таких проблем не возникает:)

                            Ну по теме — потоки на форках, это конечно очень уныло. Кстати, была на хабре похожая тема(да почти идентичная, тоже пхпшные потоки на форках), я специально не поленился и погуглил насчет php pthread. И такой проект есть, насчет его статуса не скажу, но если оно работает, это куда лучше форков:)
                            0
                            кстати, если мне не изменяет память, в случае с одноядерным процессором выйгрыш сомнителен.
                            поправьте меня, если ошибаюсь…
                              0
                              Если ожидание имеем в следствие какого-то сложного расчёта, то выигрыша и на многоядерном будет немного. А если задержка связана с сетью, вызовом системным или элементарным ожиданием, то и прирост будет огромным. Идеальный вариант это надо 1000 раз запустить скрипт вида «wait 10; echo random()», когда вопрос стоит «ждать или не ждать» и второстепенно «не жирно ли запустить параллельно 10-50-100-1000 процессов php»
                                0
                                Основная фишка тут в том, что пока один поток ждет чего-то, второй в это время выполняется. Т.е., как TDz выше написал, например работа с сетью. Один поток отправил запрос и ждет ответа, второму ответ уже пришел, и он обрабатывает результат, третий за это время успел уже два запроса обработать. В итоге, без потоков эти пять запросов обрабатывались бы последовательно, а с потоками — паралельно.
                                +1
                                Ресурсоёмкие задачи параллельно выполнять чревато упиранием в потолок по CPU/IO/OOM. А такой способ выполнения одновременно нескольки медленных задач и экономит нам задержку и утилизирует рессурсы, выполняя ту же задачу быстрее за счёт симультанных запросов, теряем небольшое кол-во памяти на дополнительные инстанции PHP правда. Для простых сетевых операций есть асинхронные сокеты (работать через которые не со всеми протоколами надо сказать удобно/возможно), для системных вызовов есть отправка в бекграунд, а для чего-то другого родного «параллелизатора» в рамках PHP нету — вот и пользуемся многозадачностью (прошу прощения термин тоже не вполне корректный), ведь задачи появляются там где не ждёшь — из последнего на моей памяти надо было получать ответ медленного CLI приложения по SSH с удалённого сервера — время коннекта плюс запуска до выдачи данных более 10 секунд, а нужно дёрнуть скажем сто раз — никакой выгоды от выдумывания варианта как сделать это всё параллельно в рамках одного запущенного PHP нету, если делать цикл будем ждать до новых веников, вот и запускается несколько процессов каждый одним потоком. Ещё думается в случае с сетевой файловой системой могут возникать лаги, которые сокетами не обойдёшь. Нельзя сказать чтоб задач было так уж много, но иногда встречаются ситуации когда запустить несколько рабочих процессов проще и быстрее чем придумывать хитросплетения многих потоков в одном процессе, пусть и в ущерб высоким принципам
                                  +1
                                  — симультанные запросы
                                  — невозможность работать с протоколами, используя асинхронные сокеты
                                  — отправка в бекграунд системных вызовов
                                  я вас не понимаю, честно :) Я старался изучать программирования всеми силами, даже Кнута читал, а не листал. но нет, всё равно встречаются программисты которых я не понимаю…
                                    0
                                    Прошу прощения если выразился неудачно, я Кнута к своему стыду даже не листал, будем думать в этом корень проблемы :)
                                    1) симультанные говоря проще есть паралельные для которых начало (более менее) одновременно, от англ. simultaneous
                                    2) медленные сетевые коннекты вы предложили параллелить используя асинхронные сокеты, и это делать в любом случае надо. Но не всегда можно распараллелить большое кол-во запросов запросов этим методом — ведь часто для работы со сложными сетевыми протоколами нам дают в руки инструменты высокого уровневя абстракции, тоесть некую блекбоксовую функцию ConnectToSomeCrazyProtocol() которая как на зло ни разу не асинхронная, и не остаётся никакой возможности использовать неблокирующие сокеты так как этого бы хотелось в рамках одного инстанса PHP.
                                    Для этого пришлось бы лезть внутрь существующего кода и чего-то там пилить. По крайней мере мой опыт подсказывает что если экшн достаточно сложен его зачастую проще распаралелить на уровне вызова новых процессов PHP чем переписывая тонкости логики. А если протокол проприетарен, либо есть необходимость использовать конкретный существующий код не предусматривающий модификаций по лицензии, либо код элементарно закрыт каким-нибудь зендовским шифровщиком, тогда самый быстрый а зачастую и один из лучших выходов будет распаралелить процессы PHP
                                    3) я говорил о вызове исполняемых файлов которые потенциально могут быть очень медлительными, system, exec и компания. Неоднократно видел в коде как решают необходимость запустить из скрипта паралельно 10 софтин — в цикле делают system("/home/user/some_soft &") получая таким образом мгновенный возврат — цель конечно достигнута, но я лично считаю это очень неудачным стилем решения вопроса, и привёл как пример воркераунда, в данном конкретном случае типичного кривого хромого quick&dirty
                                  +2
                                  Неблокирующие сокеты — это не костыли для многопоточности, они для другой цели немного служат.
                                    0
                                    По ссылке как раз для многопоточности их используют.
                                    0
                                    А я поддержу парня. Мы, например, для распараллеливания как раз подобный принцип используем, ну просто потому что пока не нашли ничего лучше.

                                    Кто знает, напишите, тогда уж альтернативу. Только на PHP, разумеется (если она есть).
                                      –1
                                      Только на PHP, разумеется (если она есть).
                                      Да, если ее нет, тогда не пишите!
                                        +2
                                        PCNTL — родной для PHP способ
                                          +2
                                          вот что знаю я:

                                          1) pcntl_fork
                                          2) запуск интерпретатора (как в примере выше)
                                          3) вызов через неблокирующие сокеты
                                          4) вызов через мультизагрузку сURL
                                          5) использовать треды (http://code.google.com/p/php-pthreads/).
                                            +1
                                            code.google.com/p/php-pthreads/ — странно, интересная должна быть разработка, но ни файлов, ни в svn нет ничего пока что
                                              0
                                              Странно, какое-то время назад, я уверен, файлы были. Что за ерунда?
                                          0
                                          Неблокирующие сокеты — костыль? Ппц
                                            0
                                            Извините, не удержался
                                            замените пожалуйста в комментах к коду
                                            «Здесь береться» на «Здесь берётся»
                                            +1
                                            а я вот давно открыл для себя PHPFork из PEAR-а, если не ошибаюсь.
                                              +1
                                              Как было сказано в одном из комментариев в упонимаемом топике, модуль cURL предоставляет возможность(хоть и не идеальную) создать много одновременных HTTP соединений. На php.net эта возможность описана очень скудно php.net/manual/en/ref.curl.php, но там же в кооментах к документации даны ссылки на вменяемые примеры исппользования:
                                              www.phpied.com/simultaneuos-http-requests-in-php-with-curl/
                                              www.rustyrazorblade.com/2008/02/curl_multi_exec/
                                              +2
                                              Автор, начните наконец пользоваться запятыми!
                                              Вот вам пачка: ,,,,,,,,,,
                                                +2
                                                Для PHP есть модуль, реализующий «честные» треды, поищите, на Хабре о нём упоминали. Ну и конечно, более чем странно упоминать запуск нескольких интерпретаторов как какое-то открытие или даже способ, настолько это простая вещь.

                                                Кстати, PCNTL (точнее — pcntl_fork), который вам часто склоняли в комментариях будет доступен только из командной строки, но скорость работы у него побольше, конечно.

                                                Так что, если вы хотели создать универсальное решение, может сделать поддержку запуска приложения, pcntl_fork, вызова себя же на неблокирующих сокетов или мультизапросами с cURL, а так же тредов через модуль? Причём свести всё вместе так, чтобы было удобно пользоваться?

                                                Мне кажется, в этом случае, народ бы точно сказал вам спасибо.
                                                  +4
                                                  Я тут не увидел threads. Даже намека на них.
                                                  Просто оболочка над streams тут.

                                                  Естественно никаких сущностей в примере нет, поэтому «почти настоящая многопоточность» в названии топика звучит очень уж лживо. До нее как раком до китая.
                                                    +4
                                                    Тут несколько процессов, а не потоков. Просто автор не понимает разницы.
                                                    0
                                                    Кстати, когда требуется многопоточность ИМХО намного лучше запускать тем же способом php скрипт вне вебсервера и делать там родной нормальный форк pcntl_fork() :)
                                                    pthread для PHP пока не сделали, а жаль :)
                                                      0
                                                      fork — не многозадачность, а pthreads был, вот тут: code.google.com/p/php-pthreads/ лежали раньше файлы.
                                                      +2
                                                      Я вот думаю, написать что-ли статью о том, что такое сабж, почему так нельзя делать и почему вообще впринципе нельзя реализовать потоки, даже модулем. И давать каждый месяц при появлении подобных статей.
                                                      • НЛО прилетело и опубликовало эту надпись здесь
                                                          –1
                                                          Напишу, но со сроками тяжко. Прошлая статья месяц ждала, причем тогда время больше терпело.
                                                          Но опыт в теме есть, так что и что написать — будет ;)
                                                        0
                                                        Как-то странно, все запущенные «процессы» отрабатывали не больше пяти секунд, а результирующее время — 5.58. Пол секунды «накладных» расходов для обеспечения работы десяти «процессов» ИМХО слишком много. Или я что-то недопонимаю?
                                                          0
                                                          здесь накладными рассходами является сам запуск php-интерпретатора.
                                                          Например на моем ноуте запуск php занимает примерно столько:
                                                          [15:38:58] max_m: ~> time php -r 'echo "1\n";'
                                                          1
                                                          
                                                          real    0m0.033s
                                                          user    0m0.020s
                                                          sys     0m0.012s

                                                          то есть 10 процессов запустились бы примерно за 0.3 сек.
                                                          Вобщем для данного метода параллелизации это нормальные цифры.
                                                          0
                                                          code.google.com/p/php-pthreads/ — странно, интересная должна быть разработка, но ни файлов, ни в svn нет ничего пока что

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

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