Pull to refresh

Простой вариант реализации многопоточности на PHP

Reading time 3 min
Views 23K
Многопоточность в PHP отсутствует «из коробки», поэтому вариантов её реализации было придумано великое множество, включая расширения pthreads, AzaThread (CThread), и даже несколько собственных наработок 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.

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

Говоря «эмуляцию» я имею в виду, что при таком методе реализации нет возможности для обмена информацией между потоками или между родительским и дочерними потоками. Он подходит в случае, если заранее известно, что такие возможности не нужны.
Tags:
Hubs:
+8
Comments 17
Comments Comments 17

Articles