Многопоточность в 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.

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

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

Комментарии (17)


  1. mikechips
    17.04.2019 16:57

    Реализация не нова и на Хабре и не такое видел.


    По сути своей это не потоки, а подпроцессы. С таким же успехом можно было использовать pcntl_fork(). В свою очередь pthreads, как мне недавно продемонстрировал один друг, работает через задницу.


    Вывод делаю простой: потоки — это не к PHP, там для этого лучше делать очереди задач или подключать другие языки.


    1. Akuma
      17.04.2019 19:00

      Уже очень долго юзаю потоки через pcntl_fork. И все в порядке и удобно.

      А вот pthreads да, там какая-то задница с автолоадом начинается. Если у вас что-то простое, то пойдет, а для полноценного приложения появляется много проблем.


      1. miksir
        17.04.2019 19:04
        +1

        pcntl_fork — это не потоки, а процессы


        1. Akuma
          17.04.2019 19:05

          Да, верно.


    1. unet900
      17.04.2019 19:00

      Расскажите подалуйста чтт не так в pthreads


      1. mikechips
        18.04.2019 03:04

        Как минимум проблемы с памятью, ООП и типом данных resource. И ад с синхронизацией.
        Нужны практические примеры?


        1. koeshiro
          18.04.2019 09:41

          Было бы интересно посмотреть. Если есть готовый пример.


    1. greatkir
      17.04.2019 22:19
      +1

      Автор pthreads написал и на данный момент предлагает использовать эту библиотеку для потоков в PHP.
      Как он признает, реализация pthreads по различным причинам действительно получалась не слишком понятной


  1. komandakycto
    17.04.2019 17:15

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


    1. mikechips
      17.04.2019 17:18

      Gearman автору в помощь)


    1. hotsanchous
      17.04.2019 21:48

      Недавно тоже раскапывал тему потоков в php и пришел к очереди (rabbitMQ). Посчитал это правильнее, чем использовать костыльные методы для многопоточности.


  1. AlexLeonov
    17.04.2019 18:09
    +1

    Никаких «потоков» здесь нет, вы просто запускаете процессы в фоне. Перенаправляя их stdout и stderr в /dev/null


  1. miksir
    17.04.2019 18:58
    +1

    Это многопроцессность (multiprocessing), а не многопоточность (multithreading). Это вполне себе определенные подходы, разница которых заключается в том, что мы используем для параллельного вычисления — процессы или потоки. В терминах операционной системы — это тоже вполне определенные (и разные) вещи. По-этому, давайте придерживаться терминологии.


  1. 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


  1. Alroniks
    18.04.2019 09:45

    Грузить бигдата целиком в память — ну такое себе решение. Это сейчас 400 мегабайт, а если данных на пару гигов? Срипт даже не запустится. Выше верно подсказали про стримы, а там уже можно и все остальное наворачивать.


    1. Tatikoma
      18.04.2019 12:29

      Зависит от задачи. У меня под некоторые задачи есть вот такой сервер:
      $ cat /proc/meminfo | grep MemTotal
      MemTotal: 396269164 kB


    1. NickyX3
      18.04.2019 12:46

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

      Q: Мужики. Могу ли я загрузить текстовый файл в 5 миллионов строк в память?
      A: А что за комп?
      Q: Sun StarFire, 32 cpu, 128 Gb RAM
      A: Тебе можно, валяй.
      (с) ru_perl 90-е годы