Основным минусом для меня стало слишком больше количество «наворотов» у этих решений — не всегда есть необходимость в обмене информации между потоками и родительским процессом или в экономии ресурсов. Всегда должна быть возможность быстро и с минимумом затрат решить задачу.
Заранее хочу оговориться, что в этом посте не открываются великие тайны — он скорее для новичков в языке, и опубликовать его я решил только потому, что в свое время сам столкнулся с проблемой и не найдя готового решения сделал эдакую эмуляцию многопоточности самостоятельно.
Итак, задача состоит в том, что бы обработать большое количество данных, пришедших в наш скрипт. Моей задачей было обработать JSON массив текстовой информации, переварив которую скрипт должен был собрать из неё не менее большой коммит для PostgreSQL.
Первым делом собираем данные в родительском файле:
index.php
// bigdata.json - файл с входными данными. Это может быть что угодно - файл, таблица в СуБД и т.д.
$big_json = file_get_contents('bigdata.json');
$items = json_decode($big_json, true);
// хоть в php и есть сборщик мусора, но лучше подчистить неиспользуемые, большие, хвосты
unset($big_json);
// ...
Размер массива колебался около 400мб (позже сократился до ~50мб), и вся информация была текстовой. Не сложно прикинуть скорость, с которой это всё переваривалось, а если учесть, что скрипт выполнялся по cron каждые 15 минут, а вычислительная мощность была такой себе — быстродействие страдало очень сильно.
После получения данных можно прикинуть их объем и при необходимости рассчитать необходимое количество потоков на каждое ядро ЦП, а можно просто решить, что потоков будет 4 и посчитать количество строк для каждого потока:
index.php
// ...
$threads = 4;
$strs_per_thread = ceil(count($items) / $threads);
// для запуска в ручном режиме - немного информации
echo "Items: ".count($items)."\n";
echo "Items per thread: ".$strs_per_thread."\n";
echo "Threads: ".$threads."\n";
// ...
Стоит сразу оговориться — такой расчет «в лоб» не даст точного результата по количеству элементов для каждого потока. Он нужен скорее для упрощения расчетов.
А теперь самая суть — создаем задачи для каждого потока и запускаем его. Делать мы это будем «в лоб» — создавая задачу для второго файла — thread.php. Он будет выступать в роли «потока», получая диапазон элементов для обработки и запускаясь независимо от основного скрипта:
index.php
// ...
for($i = 0; $i < $threads; $i++){
if($i == 0) {
passthru("(php -f thread.php 0 ".$strs_per_thread." & ) >> /dev/null 2>&1");
}
if($i == $threads-1) {
passthru("(php -f thread.php ".($strs_per_thread * $i)." ".count($items)." & ) >> /dev/null 2>&1");
}
if(($i !== 0)&&($i !== $threads-1)) {
$start = $strs_per_thread * $i + 1;
$end = $start -1 + $strs_per_thread;
passthru("(php -f thread.php ".$start." ".$end." & ) >> /dev/null 2>&1");
}
}
// ...
Функция passthru() используется для запуска консольных команд, но скрипт будет ждать окончания выполнения каждой из них. Для этого мы оборачиваем команду на запуск в набор операторов, которые запустят процесс и тут же вернут ничего, запустив процесс и родительский процесс не приостановится в ожидании выполнения каждого дочернего:
# вся магия, как это часто бывает, в самом Linux-е
(php -f thread.php start stop & ) >> /dev/null 2>&1
Что конкретно тут происходит, к сожалению, точно сказать не могу — набор параметров мне подсказал мой знакомый линуксоид. Если в комментах сможете расшифровать эту магию — буду признателен и дополню пост.
Файл thread.php:
$start = $argv[1];
$stop = $argv[2];
for ($i = $start; $i <= $stop; $i++) {
// какие-то действия с каждым элементом массива или строки из СуБД
}
Вот таким, довольно простым, способом можно реализовать эмуляцию многопоточности в PHP.
Если сократить весь пример до сухого вывода, то думаю он звучал бы так: родительский поток через командную строку запускает дочерние процессы, указывая им какую именно информацию обработать.
Говоря «эмуляцию» я имею в виду, что при таком методе реализации нет возможности для обмена информацией между потоками или между родительским и дочерними потоками. Он подходит в случае, если заранее известно, что такие возможности не нужны.
Комментарии (17)
komandakycto
17.04.2019 17:15Думаю вместо того, что написал автор, надо брать очереди и писать воркеры. Получится прямее. Вы передаете диапазон данных, которые должен обработать один из экземпляров скрипта, однако может произойти ошибка по середине. Нужно понимать как обработать оставшиеся данные, на каком месте мы остановились, как поднять скрипт с места остановки.
hotsanchous
17.04.2019 21:48Недавно тоже раскапывал тему потоков в php и пришел к очереди (rabbitMQ). Посчитал это правильнее, чем использовать костыльные методы для многопоточности.
AlexLeonov
17.04.2019 18:09+1Никаких «потоков» здесь нет, вы просто запускаете процессы в фоне. Перенаправляя их stdout и stderr в /dev/null
miksir
17.04.2019 18:58+1Это многопроцессность (multiprocessing), а не многопоточность (multithreading). Это вполне себе определенные подходы, разница которых заключается в том, что мы используем для параллельного вычисления — процессы или потоки. В терминах операционной системы — это тоже вполне определенные (и разные) вещи. По-этому, давайте придерживаться терминологии.
Samouvazhektra
17.04.2019 20:43Эмуляция это конечно хорошо, но для обработки жирных Json есть либы базирующиеся на стримах
https://github.com/kuma-giyomu/JSONParser
https://github.com/salsify/jsonstreamingparser
https://github.com/halaxa/json-machine
https://github.com/violet-php/streaming-json-encoder
Alroniks
18.04.2019 09:45Грузить бигдата целиком в память — ну такое себе решение. Это сейчас 400 мегабайт, а если данных на пару гигов? Срипт даже не запустится. Выше верно подсказали про стримы, а там уже можно и все остальное наворачивать.
Tatikoma
18.04.2019 12:29Зависит от задачи. У меня под некоторые задачи есть вот такой сервер:
$ cat /proc/meminfo | grep MemTotal
MemTotal: 396269164 kB
NickyX3
18.04.2019 12:46Грузить бигдата целиком в память — ну такое себе решение.
Q: Мужики. Могу ли я загрузить текстовый файл в 5 миллионов строк в память?
A: А что за комп?
Q: Sun StarFire, 32 cpu, 128 Gb RAM
A: Тебе можно, валяй.
(с) ru_perl 90-е годы
mikechips
Реализация не нова и на Хабре и не такое видел.
По сути своей это не потоки, а подпроцессы. С таким же успехом можно было использовать pcntl_fork(). В свою очередь pthreads, как мне недавно продемонстрировал один друг, работает через задницу.
Вывод делаю простой: потоки — это не к PHP, там для этого лучше делать очереди задач или подключать другие языки.
Akuma
Уже очень долго юзаю потоки через pcntl_fork. И все в порядке и удобно.
А вот pthreads да, там какая-то задница с автолоадом начинается. Если у вас что-то простое, то пойдет, а для полноценного приложения появляется много проблем.
miksir
pcntl_fork — это не потоки, а процессы
Akuma
Да, верно.
unet900
Расскажите подалуйста чтт не так в pthreads
mikechips
Как минимум проблемы с памятью, ООП и типом данных resource. И ад с синхронизацией.
Нужны практические примеры?
koeshiro
Было бы интересно посмотреть. Если есть готовый пример.
greatkir
Автор pthreads написал и на данный момент предлагает использовать эту библиотеку для потоков в PHP.
Как он признает, реализация pthreads по различным причинам действительно получалась не слишком понятной