Avatar ·

Как реализовать многопоточность в PHP приложениях?

Всем привет! Я пишу на PHP уже около года, и столкнулся с проблемой, которую не могу решить. Мне нужно обработать большой массив данных (около 100 000 записей из базы данных), для каждой записи нужно выполнить HTTP-запрос к внешнему API и сохранить результат. В цикле это работает, но очень медленно — один запрос занимает около 0.5-1 секунды, и вся обработка может занять несколько часов. Я попробовал использовать `curl_multi_exec` для параллельных запросов, но это не совсем то, что нужно, потому что мне нужно выполнять не только HTTP-запросы, но и другую работу (например, парсинг HTML, запись в файлы, обработку изображений). Вот пример моего текущего кода: ```php $data = getLargeArrayFromDB(); // 100k записей foreach ($data as $item) { $result = callExternalAPI($item['id']); // медленный HTTP-запрос processResult($result); // еще немного работы saveToFile($result); } ``` Я слышал, что в других языках (Java, Python) есть нормальная многопоточность, а в PHP — нет. Но видел библиотеки типа `pthreads` и `parallel`. Подскажите, как правильно организовать многопоточную обработку в PHP? Может быть, есть другие подходы, кроме классических потоков? Заранее спасибо! <<>>

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

Вы правы — в PHP нет нативной многопоточности в том виде, как в Java или C++. Каждый PHP-скрипт выполняется в одном потоке. Однако это не значит, что нельзя организовать параллельную обработку. Есть несколько проверенных подходов, и мы рассмотрим лучший из них для вашей задачи — использование расширения parallel (доступно с PHP 7.4+).

Решение с помощью расширения parallel

Расширение parallel предоставляет API для создания и управления потоками (Thread) и каналов (Channel) для обмена данными. Это идеально подходит для вашей задачи: вы можете разбить массив на части, обработать каждую часть в отдельном потоке, а затем собрать результаты.

Установка (на Linux):

sudo pecl install parallel

Добавьте extension=parallel.so в php.ini.

Пример реализации

<?php
// Исходные данные (в вашем случае 100k записей)
$allData = getLargeArrayFromDB(); // предположим, это массив

// Количество потоков (зависит от CPU и кол-ва ядер)
$numThreads = 8;

// Разбиваем данные на части
$chunks = array_chunk($allData, ceil(count($allData) / $numThreads));

// Создаем каналы для сбора результатов
$channel = new \parallel\Channel($numThreads);

// Функция, которая будет выполняться в каждом потоке
$worker = function(array $items, \parallel\Channel $channel) {
    $results = [];
    foreach ($items as $item) {
        // Ваша логика: HTTP-запрос, обработка, сохранение
        $result = callExternalAPI($item['id']);
        $processed = processResult($result);
        saveToFile($processed);
        $results[] = $processed;
    }
    // Отправляем результат обратно в основной поток
    $channel->send($results);
};

// Запускаем потоки
$futures = [];
foreach ($chunks as $chunk) {
    $futures[] = \parallel\run($worker, [$chunk, $channel]);
}

// Ждем завершения всех потоков
foreach ($futures as $future) {
    $future->value(); // блокируется до завершения потока
}

// Собираем результаты из канала
$allResults = [];
for ($i = 0; $i < $numThreads; $i++) {
    $allResults = array_merge($allResults, $channel->recv());
}

// Закрываем канал
$channel->close();

echo "Обработано записей: " . count($allResults);
?>

Альтернатива: Process Pool (для более сложных сценариев)

Если нужно управлять очередью задач, используйте \parallel\bootstrap и пул потоков:

<?php
$runtime = new \parallel\Runtime();
$future = $runtime->run(function() {
    // Код, выполняемый в отдельном потоке
    return heavyComputation();
});
$result = $future->value();
?>

Важные замечания

  • Безопасность потоков: PHP не потокобезопасен (ZTS не гарантирует безопасность). Используйте parallel только для изолированных задач, без общего состояния.
  • Ограничения: Нельзя передавать замыкания с внешними переменными (кроме примитивов). Используйте use или передавайте данные через каналы.
  • Ресурсы: Каждый поток — отдельный процесс PHP, поэтому потребление памяти умножается на количество потоков.
  • Ошибки: Обрабатывайте исключения внутри потока, иначе они "потеряются".

Когда использовать другие подходы

  • curl_multi_exec — если задача сводится только к HTTP-запросам (без дополнительной обработки).
  • pcntl_fork — для Unix-систем, требует осторожности с разделяемыми ресурсами.
  • Очереди (RabbitMQ, Redis) — для распределенной обработки на нескольких серверах.

В вашем случае parallel — оптимальный выбор. Попробуйте разбить 100k записей на 8-16 потоков, и время обработки сократится с часов до минут (в зависимости от скорости API).

<<>>

Альтернативный подход: использование процессов через pcntl_fork

Если расширение parallel недоступно (например, на старых версиях PHP или shared-хостинге), можно использовать системные вызовы через pcntl_fork. Этот метод работает только на Unix-подобных системах.

Пример реализации:

<?php
$data = getLargeArrayFromDB();
$chunks = array_chunk($data, 1000);
$processes = [];

foreach ($chunks as $chunk) {
    $pid = pcntl_fork();
    if ($pid == -1) {
        die('Ошибка форка');
    } elseif ($pid) {
        // Родительский процесс: сохраняем PID
        $processes[] = $pid;
    } else {
        // Дочерний процесс: обрабатываем свой кусок
        foreach ($chunk as $item) {
            $result = callExternalAPI($item['id']);
            processResult($result);
            saveToFile($result);
        }
        exit(0); // завершаем дочерний процесс
    }
}

// Ждем завершения всех дочерних процессов
foreach ($processes as $pid) {
    pcntl_waitpid($pid, $status);
}
?>

Недостатки метода

  • Сложность управления разделяемыми ресурсами (файлы, БД).
  • Каждый процесс — копия родителя, что увеличивает потребление памяти.
  • Нет встроенных каналов для обмена данными (нужно использовать семафоры, очереди или временные файлы).

Этот подход подходит для простых задач, где дочерние процессы работают изолированно и не обмениваются данными. Для вашей задачи с 100k записей и необходимостью сбора результатов лучше использовать parallel или очереди сообщений.