Comments 62
Всё-таки, если порядок обработки заданий не важен, то это не очередь с заданиями, а пул с заданиями. Лучше использовать корректную терминологию.
Вопрос от человека, ни разу не работавшего с потоками: есть ли у такого подхода еще какие-то преимущества, помимо производительности?
UFO landed and left these words here

Это собственно причина того что производительность улучшается. Но опять же нужно учитывать еще локи, переключение контекста… Словом в WEB оно не столь разумно как event loop в подавляющем большинстве случаев.

UFO landed and left these words here
всякие выгрузки на PHP и суют их уже в крон.

а это не сетевое взаимодействие? Там простоев из-за сетевых запросов обычно больше чем CPU-time на работу самого пыха.


Короче есть где применить, нужно просто иметь ввиду, что есть такая классная штука.

Иметь в виду — конечно стоит. Но нужно так же знать о других вариантах (мультиплексирование потока выполнения, корутины, очереди + процессы) и выбирать из них. А так для тредов есть свои задачи.

У каждого подхода свои преимущества и недостатки. Подходы:


  • Создаем общедоступную очередь, например, на Beanstalk, RabbitMQ или Redis или еще на чем-нибудь. Создаем PHP скрипт, который будем запускать из консоли несколько раз, создавая нужное количество процессов. Это решение наиболее универсальное.
    • Плюсы. Хорошая масштабируемость на несколько серверов, отказоустойчивость.
    • Минусы. Может быть неудобно или непрозрачно с точки зрения архитектуры. Если в обработке данных несколько “узких горлышек”, то возможно, понадобится предусмотреть несколько очередей.
  • Создавать потоки через Curl, для такого решения есть даже проект на гитхабе.
    • Плюсы. Мне неизвестны.
    • Минусы. Ненадежно.
  • Использовать popen().
    • Плюсы. Просто с первого взгляда.
    • Минусы. Сложно организовать равномерную загрузку ядер. Трудности в создании общей очереди.
  • Написать собственное расширение для PHP и пользоваться им.
    • Плюсы. Можно сделать полный фен шуй.
    • Минусы. Затратно.
  • Воспользоваться расширением PCNTL. Насколько это удачное решение, возможно, кто-то расскажет в комментариях.
  • Воспользоваться готовым расширением pthreads.
    • Плюсы. Надежность. Можно прятать многопоточное поведение внутри модуля, не выносить на уровень архитектуры. Простота в создании общей очереди.
    • Минусы. Нельзя масштабировать на несколько серверов.

Все кроме последнего никакого отношения к потокам не имеет. Это порождение процессов.


Минусы. Сложно организовать равномерную загрузку ядер. Трудности в создании общей очереди.

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


Написать собственное расширение для PHP и пользоваться им.

Все уже написано. Имеет смысл только написало аналог микротредов (корутины + пул тредов), но в теории это можно и на userland сделать.


Воспользоваться расширением PCNTL

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


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


Воспользоваться готовым расширением pthreads.

На самом деле для WEB в 95% случаев все упирается в эффективность работы с I/O и тут явный лидер корутины/event loop так как нет накладных расходов на переключение контекстов. А что бы эффективнее использовать ресурсы можно просто увеличить количество процессов.


Треды хорошо подходят для каких-то массивных вычислений, хотя тут уже вопрос зачем нам PHP если мы можем написать многопоточную програмку на Си с векторизацией вычислений и получить 100x профита.


Так что использование тредов в PHP я считаю экзотикой нежели чем-то важным и необходимым. Хотя понимать минусы использования тредов — это важно.

хотя тут уже вопрос зачем нам PHP если мы можем написать многопоточную програмку на Си с векторизацией вычислений и получить 100x профита

Ну например если весь проект на PHP, то зачем для одной задачи искать программера на Си. На первое время решения на PHP хватит с головой, тем более, если речь о PHP 7.
На первое время решения на PHP хватит с головой, тем более, если речь о PHP 7.

Если речь идет про объемные вычисления, они для начала должны легко паралелиться, иначе объем работ на паралелизацию может слихвой покрыть разницу php vs С.


Если алгоритм легко паралелится — то никаких проблем но "первое время" может быстро закончится если мы зависим от количества данных и оно увиличивается. У меня был на проектике скриптик с k-means, написанный на коленке потому что так быстрее. Его "первое время" закончилось через 2 недели, когда обработка данных стала занимать по 5 минут на запуск (100КК итераций). Переход на PHP7 а потом на HHVM снизил время в 2 раза но с объемами данных этот "профит" быстро бы невилировался. Распаралелить его обошлось бы довольно дорого, в итоге просто применили другой алгоритм кластеризации реализованный на java (потому что готовый и потому что реализовывать его на PHP сильно дорого вышло бы).

Скажем так, у меня была такая задача, реализована на PCNTL, суть заключалась в том, что регулярно поступает достаточно большой набор ссылок на картинки для каталога товаров из импорта и требуется произвести конвертацию в формат сайта. Деление на задачи производится по домену и у каждого запускается свой пул потоков в соответствии с настройками. В рамках нагрузки на систему, каждый поток незначительный, но требуется обработать максимум в кратчайшие сроки. На мой взгляд дешевле и быстрее такое сделать на PHP, чем на Си.

Если использовать расширение pthreads + PCNTL, то можно сократить количество процессов и выиграть в производительности
На мой взгляд дешевле и быстрее такое сделать на PHP, чем на Си.

На мой взгляд вам в вашей задаче потоки не нужны. Берем очередь, берем парочку процессов-воркеров обрабатывающих очередь, в каждом воркере будут крутиться корутины/event loop (amphp, reactphp, recoil). Итого имеет малое количество процессов, отсутствие оверхэда на создание потоков/переключение контекста, отсутствие блокировок, максимальную утилизацию CPU, максимальный перформанс. Ну и делать это даже проще чем на тредах.

А если например нужно использовать mysqli?
На пример: задача в том, чтобы в несколько потоков брать записи из одной таблицы, проводить манипуляции над данными, а результат записывать в другую таблицу в произвольном порядке?
При этом потоки не должны читать одну и ту-же запись, но должны гарантированно прочитать все записи.
Объект mysqli нельзя «расшаривать» между потоками.
1. создаёте mysqli объект после разделения на потоки — у каждого потока будет своё подключение.
2. делаете очередь в базе, там дополнительные столбцы: «бот который взял на обработку».
в момент взятия задачи на обработку, делаете лок:
update table set bot=BOT_ID where bot=0;

3. В добавок, нужно сделать какой нибуть механизм, который будет разлочивать строки ботов которые зависли.
Я вместо этого делал так: каждый поток читает все строки таблицы, но обрабатывает только те, которые должен.
Ну там несложные вычисления: каждый поток знает свой номер и общее количество потоков, ну и отсчитывает каждую X строчку со сдвигом Y
Делал примерно так же, но присваивал значение timestamp. И отдельным потоком просматривал все записи с значением меньше, чем «timestamp — время на таймаут», если такие появлялись — давал им еще несколько попыток обнуляя значение, затем блокировка записи с присвоением значения заведомо бОльшим.
У меня было реализовано так, по некому группирующему параметру из источника данных создавалась задача, в отдельной таблице. Далее, при чтении из источника, полученная строка сразу же удалялась. Одновременно может работать любое количество потоков без пересечения. Я делал это на Redis, там своя специфика работы на сокетах и как такового persistent connection просто не существует. В MySQL таких проблем нет.
Если не удалять записи, а устанавливать некий параметр, аля bot_id, то всё равно нужно как-то чистить обработанные записи, чтобы не перегружать таблицу.
В вашем случае, есть возможность что задача будет удалена, хотя она не была выполнена (Т.е. создаём задачу, бот её сразу же забирает, и потом бот почему то падает.). Лучше удалять задачу после того как она взята и выполнена. Т.е. когда «берём задачу» — её лочим, но не удаляем. Когда сделали — удаляем.
включен XDebug например

Замерам и выводам тогда не стоит верить. XDebug искажает картину полностью.

Основные сравнения тут без XDebug. С XDebug только 2 довольно синтетических теста для оценки потерь от использования polyfill.

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

Это вполне себе удачное решение. Только вы должны понимать, что pcntl — это многопроцессность, а не многопоточность.

Плюсы:
  • Процессы независимы, каждый из них выполняется изолированно, его время жизни никак не зависит от других процессов
  • Расширение pcntl работает везде и из коробки (кроме Windows по понятным причинам)


Минусы:
  • Форк — не самая дешевая операция
  • N процессов требуют *N памяти
  • Межпроцессное взаимодействие вам нужно выстраивать самостоятельно
  • После форка дочерний процесс теряет контекст (подключения к файлам, БД, прочим ресурсам), его нужно восстанавливать


В целом мне pcntl нравится и он находит своё применение
Похоже, это связано с тем, что физических ядра у моего процессора 4

так и есть, все рекомендации сводятся к тому, чтоб запускать по одному потоку на ядро

Это связано с переключением контекста. Чем больше потоков, тем чаще нам нужно переключаться, а операция эта не дешевая.

Всё зависит от того, чем потоки занимаются. У меня сейчас на 8 ядрах запущено 2600+ потоков и всё летает. А если 8 потоков заняты вычислениями, загружая каждое ядро на 100%, то, естественно, что не делай, они не начнут быстрее выполнять свою работу.
Почему? Даже если открыть N коннектов и работать с каждыми из них по отдельности?

Приминимо, просто нужно держать не один коннекшен к базе, а пул коннекшенов.

Проверял, точно не падает?
когда тестировал, у меня упало…

используя неблокируемое соединение — не обязательно делать несколько воркеров,
тут совсем другой код…

если кто и проверял — путь выложат код в студию…
точно не падает?

почему не падает? Падает. Только в моем случае из-за базы не падало.


используя неблокируемое соединение — не обязательно делать несколько воркеров

используя треды не обязательно делать пул соединений с базой ибо… ну воркеры всеравно изолированные. Шаред мемори только в явном виде и только то что можно сериализовать.


если кто и проверял — путь выложат код в студию…

Вам код чего? Ну мол примерно что бы понимать минимальный набор функционала что бы убедиться что… ну не ок все а можно хотя бы пробовать.

> что бы убедиться что…
параллельное выполнение запросов в БД
К счастью с базой можно работать в несколько потоков еще в php5 — можно использовать mysqli с его неблокирующими запросами к БД.
Бесспорно полезное расширение, однако надо всегда держать в уме, что PHP интерпретируемый язык, который итак имеет достаточно накладных расходов на выполнение своих скиптов. Так что распараллеливание может и дать достаточное ускорение работы, но при этом забрать существенно больше ресурсов чем ожидается, так как все обертка на posix threads все же будет добавлять некий оверхед. Так же, если есть нужна проводить тяжелые операции на сервере (которые требуют оптимизации распараллеливанием), то это повод задуматься как это в будущем будет развиваться и, возможно, стоит какую то часть функционала переписать в виде C расширения, например.
так как все обертка на posix threads все же будет добавлять некий оверхед

Незначительный, им можно принебречь. Но в остальном согласен, event loop справляется лучше, а если нужно нагрузить все ядра — просто делаем больше процессов.

Все бы хорошо, если не большое количчество нюансов, ограничей и отличия работы PHP c pthreads от PHP как такового.
Нельзя быть уверенным, что стандартная языковая конструкция будет работать корректно.
Пример: https://github.com/krakjoe/pthreads/issues/52
И многое другое, типа позднего статического связываня и наследования… Достаточно взглянуть https://github.com/krakjoe/pthreads/issues

Как эксперимент, очень интересная библиотека. В продакшен?… врядли.
Промахнулся со статическим связыванием, проблема со статическими свойствами.

pthreads хорош, но стоит упомянуть и про подводные камни.


Есть возможности "пронаследовать" какую-то часть окружения в поток (причём, по умолчанию это не "ничего"), но в ряде случаев вылезают WTF. Например, если в мастере был подключен автозагрузчик композера через require_once, он не подключится в потоке аналогичном образом.


Ресурсы, файловые дескрипторы и прочее не шарятся. Потому лучше стараться запускать потоки через PTHREADS_INHERIT_NONE, а внутри производить собственные подключения к БД, логи (монолог умеет писать с блокировками) и т.д.


Общение между потоком и мастер-процессом происходит с чем-то вроде сериализации, потому вы не сможете вернуть в мастер Closure, т.к. тот не сериализуем. Есть особенности и с другими типами данных, например с массивами.


Если вы захотите вернуть из потока вложенный массив и сделаете так.


$this->result = ['hello' => ['foo' => 'bar']]

То можете словить ошибку, так как это будет преобразовано в Volatile-объекты и при попытке считать данные в мастере они будут уже уничтожены сборщиком мусора.


Самый простой "способ" в таком случае, это явно приводить к массиву:


$this->result = (array) ['hello' => ['foo' => 'bar']]

Подробнее здесь: http://stackoverflow.com/questions/14796674/a-php-pthreads-thread-class-cant-use-array


В целом, от меня общий совет — каждый поток должен быть максимально независим от мастер процесса. Обмен, по возможности, производить скалярными данными.


Также на некоторых конфигурациях систем (например у меня такое происходит Debian 7 и pthreads 3) могут вылетать ошибки сегментирования. С чем это точно связано я не знаю, но скорее всего с версиями каких-то библиотек.

Молодец Николай, редкую тему поднял (в контексте php/pthreads) и с конкретными примерами — привет от бывшего коллеги.
Коля, я бы добавил на графики шкалу времени, чтобы был понятен масштаб. Вообще, это типичный вопрос по графикам — что отложено по осям :)

Да, можно было. Там по горизонтали секунды, вертикальное деление — это 10 секунд. По вертикали % загрузки.
image

Невероятно! Надо бы им на досуге интро на php.net перевести в качестве благодарности…
Судя по графикам у меня создалось впечатление, что это все равно не мультипоточность, а мультипроцессовость, которые в PHP почему-то постоянно путают.

Автор расширения пишет, что именно многопоточность.


This project provides multi-threading that is compatible with PHP based on Posix Threads.

На самом деле разница между многопроцессностью и многопоточностью проявляется только в общем потреблении ресурсов (потоки меньше жрут, что как бы логично) и в меньшем времени, необходимом на старт. А графики выглядели бы примерно так же. Просто с потоками не нужно париться о организации IPC

Тут речь как раз, именно, о multi-threading — основанной на самой популярной в *nix/C реализации Posix Threads.

На сколько мне память не изменяет — потоки создаются в контексте процесса и шарят память между собой.
То есть, запустили браузер — пошел процесс, а в нем уже threads.
Разница есть и она существенная, иначе парадигма существования thread-ов была бы обречена.

Так же есть отличные возможности lock/unlock мониторов (по-крайней мере в С), то есть anti-deadlock механики, ожидания завершений других потоков и т.д…
Пришел в голову один вариант использования, подскажите, пожалуйста, возможно использовать многопоточность в данном случае или нет?

Во многих проектах при определенном действии нужно отправить уведомление через email или SMS через какой-то сервис. Тут либо все делать синхронно, либо использовать очереди (Beanstalkd, Amazon SQS и т.д.). Можно ли вместо этого использовать отдельный поток, который отправит все необходимое, а основной поток вернет сообщение об успешной операции?
а основной поток вернет сообщение об успешной операции?


так это ж дожидаться надо, что собственно не сильно эффективнее просто синхронного вызова. Поток будет создан то в контексте обработки одного запроса, а потому наиболее эффективным вариантом будет организовать ивент луп в отдельном процессе воркере который будет забирать задачи из beanstalkd.
Я имел в виду, что основной поток просто даст сигнал на отправку уведомлений и сразу займется другим делом, не дожидаясь пока письмо и СМС будут успешно (или неуспешно) отправлены.

Повторюсь. Основная проблема — умирающая модель выполнения пыха. После окончания обработки запроса процесс умрет а вместе с ним и треды. Если же у вас используется какой php-pm или reactphp это вполне себе осуществимо.


к слову в Symfony так осуществляется отправка email-ов. Вместо того что бы сразу его отправлять задача попадает в очередь (просто массивчик) и после того как респонс ушел на клиент, отправляется сообщение через SAPI о том что больше данных не будет поступать на клиент, соединение закрывается, и мы начинаем отправку почты. В итоге суммарное время выполнения скрипта такое же, но время обработки респонса меньше.

C:\test\pthreads>php index2.php (without pthreads)
0.31901907920837

C:\test\pthreads>php index.php (with pthreads)
2.0081150531769

C:\test\pthreads>php -v
PHP 7.0.13 (cli) (built: Nov 8 2016 13:33:54) ( ZTS )
Copyright © 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright © 1998-2016 Zend Technologies
with Zend OPcache v7.0.13, Copyright © 1999-2016, by Zend Technologies

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

Я правильно понял, что можно создать любое количество потоков, а не только количество равное ядрам процессора? То есть это как pcntl, только создаются не отдельные процессы, а потоки?
Only those users with full accounts are able to leave comments. Log in, please.