Многопоточность в PHP отсутствует «из коробки», поэтому вариантов её реализации было придумано великое множество, включая расширения pthreads, AzaThread (CThread), и даже несколько собственных наработок PHP программистов.
Основным минусом для меня стало слишком больше количество «наворотов» у этих решений — не всегда есть необходимость в обмене информации между потоками и родительским процессом или в экономии ресурсов. Всегда должна быть возможность быстро и с минимумом затрат решить задачу.
Заранее хочу оговориться, что в этом посте не открываются великие тайны — он скорее для новичков в языке, и опубликовать его я решил только потому, что в свое время сам столкнулся с проблемой и не найдя готового решения сделал эдакую эмуляцию многопоточности самостоятельно.
Итак, задача состоит в том, что бы обработать большое количество данных, пришедших в наш скрипт. Моей задачей было обработать JSON массив текстовой информации, переварив которую скрипт должен был собрать из неё не менее большой коммит для PostgreSQL.
Первым делом собираем данные в родительском файле:
index.php
Размер массива колебался около 400мб (позже сократился до ~50мб), и вся информация была текстовой. Не сложно прикинуть скорость, с которой это всё переваривалось, а если учесть, что скрипт выполнялся по cron каждые 15 минут, а вычислительная мощность была такой себе — быстродействие страдало очень сильно.
После получения данных можно прикинуть их объем и при необходимости рассчитать необходимое количество потоков на каждое ядро ЦП, а можно просто решить, что потоков будет 4 и посчитать количество строк для каждого потока:
index.php
Стоит сразу оговориться — такой расчет «в лоб» не даст точного результата по количеству элементов для каждого потока. Он нужен скорее для упрощения расчетов.
А теперь самая суть — создаем задачи для каждого потока и запускаем его. Делать мы это будем «в лоб» — создавая задачу для второго файла — thread.php. Он будет выступать в роли «потока», получая диапазон элементов для обработки и запускаясь независимо от основного скрипта:
index.php
Функция passthru() используется для запуска консольных команд, но скрипт будет ждать окончания выполнения каждой из них. Для этого мы оборачиваем команду на запуск в набор операторов, которые запустят процесс и тут же вернут ничего, запустив процесс и родительский процесс не приостановится в ожидании выполнения каждого дочернего:
Что конкретно тут происходит, к сожалению, точно сказать не могу — набор параметров мне подсказал мой знакомый линуксоид. Если в комментах сможете расшифровать эту магию — буду признателен и дополню пост.
Файл thread.php:
Вот таким, довольно простым, способом можно реализовать эмуляцию многопоточности в PHP.
Если сократить весь пример до сухого вывода, то думаю он звучал бы так: родительский поток через командную строку запускает дочерние процессы, указывая им какую именно информацию обработать.
Говоря «эмуляцию» я имею в виду, что при таком методе реализации нет возможности для обмена информацией между потоками или между родительским и дочерними потоками. Он подходит в случае, если заранее известно, что такие возможности не нужны.
Основным минусом для меня стало слишком больше количество «наворотов» у этих решений — не всегда есть необходимость в обмене информации между потоками и родительским процессом или в экономии ресурсов. Всегда должна быть возможность быстро и с минимумом затрат решить задачу.
Заранее хочу оговориться, что в этом посте не открываются великие тайны — он скорее для новичков в языке, и опубликовать его я решил только потому, что в свое время сам столкнулся с проблемой и не найдя готового решения сделал эдакую эмуляцию многопоточности самостоятельно.
Итак, задача состоит в том, что бы обработать большое количество данных, пришедших в наш скрипт. Моей задачей было обработать 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.
Если сократить весь пример до сухого вывода, то думаю он звучал бы так: родительский поток через командную строку запускает дочерние процессы, указывая им какую именно информацию обработать.
Говоря «эмуляцию» я имею в виду, что при таком методе реализации нет возможности для обмена информацией между потоками или между родительским и дочерними потоками. Он подходит в случае, если заранее известно, что такие возможности не нужны.
