Пакет · thread

Установка и требования

Winter Thread рассчитан на POSIX-окружение PHP CLI. Установите его одной командой Composer, убедитесь, что присутствует пара расширений, — и для большинства приложений не пишите вообще никакой конфигурации.

Установка

bash
composer require flytachi/winter-thread

Установка пакета также добавляет bootstrap-скрипт для дочернего процесса как vendor/bin/wRunner. Вы никогда не вызываете его вручную — engine делает это за вас, — но он должен оставаться исполняемым и доступным на диске (см. Путь к runner ниже).

Требования

Требование Зачем Обязательно?
PHP >= 8.4 современные возможности языка (readonly-классы, синтаксис first-class callable, именованные аргументы) Обязательно
ext-pcntl pcntl_fork() для detached-режима Обязательно (Composer)
ext-posix posix_kill() (сигналы) и posix_setsid() (detached-режим) Обязательно (Composer)
opis/closure ^4.5 безопасная сериализация замыканий / анонимных классов и подписанные payload Обязательно (Composer)
ext-shmop только для транспорта через разделяемую память (ShmTransport) Опционально

ext-pcntl и ext-posix — это стандартные, лёгкие POSIX-расширения, входящие почти в каждую сборку PHP CLI под Linux/macOS — ничего экзотического. Нет требования к ZTS-сборке и нет тяжёлых расширений (swoole / parallel / pthreads).

opis/closure — это жёсткая зависимость, устанавливаемая автоматически командой composer require flytachi/winter-thread, отдельного шага нет. Именно она позволяет сериализовать анонимные классы и объекты Closure и выполнять их в фоновом процессе, а также подписывает payload, когда задан секрет; штатный serialize() в PHP не умеет работать с замыканиями или class@anonymous и падает на них.

Проверить установленные расширения можно через php -m:

bash
php -m | grep -E 'pcntl|posix|shmop'

Что обеспечивает каждая зависимость

Базовый путь запуска/ожидания (start()join()/reap()) требует только proc_open. ext-posix обеспечивает управление сигналами (pause, resume, interrupt, terminate, kill); ext-pcntl обеспечивает fork для detached-режима. Пакет требует оба, чтобы полный набор возможностей работал всегда — если на платформе отсутствует одно, затрагивается лишь соответствующая функция.

ext-shmop (опционально)

Транспорт через разделяемую память (ShmTransport, полезен под Swoole) — единственная функция, которой нужен ext-shmop. Он проверяется во время выполнения: если расширения нет, при подготовке или получении payload выбрасывается понятное исключение ThreadException ("ShmTransport requires ext-shmop.") — а не фатальная ошибка. Если он недоступен, используйте TempFileTransport, которому не нужно дополнительное расширение. См. Доставку payload.

Операционная система

Winter Thread рассчитан на POSIX-совместимую ОС (Linux, macOS, BSD). Он опирается на POSIX-сигналы, setsid и /proc, разрабатывается и тестируется на Linux и macOS. Windows не поддерживается.

Настройка bootstrap

Конфигурация действует на весь процесс и живёт в Engine — объекте-стратегии на стороне родителя, который решает, какой бинарник PHP, runner-скрипт и транспорт payload использует Thread. Привяжите его один раз во время bootstrap вашего приложения.

Без конфигурации (рекомендуется)

Для большинства приложений настраивать нечего. Если вы никогда не привязываете engine, первый Thread лениво создаёт AdaptiveEngine по умолчанию, который самонастраивается под найденный рантайм — обычный CLI, PHP-FPM/CGI или активный рантайм Swoole. Просто запускайте потоки:

php
<?php
require 'vendor/autoload.php';

use Flytachi\Winter\Thread\Runnable;
use Flytachi\Winter\Thread\Thread;

$thread = new Thread(new class implements Runnable {
  public function run(array $args): void { /* работа здесь */ }
});

$thread->start();

Явный engine

Чтобы взять управление на себя, соберите engine и привяжите его через Thread::bindEngine(). Используйте ManualEngine для чистого листа, где вы задаёте каждую часть сами, или AdaptiveEngine с именованными аргументами, чтобы переопределить только то, что нужно.

php
<?php

use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Engine\ManualEngine;
use Flytachi\Winter\Thread\Payload\TempFileTransport;

Thread::bindEngine(
  (new ManualEngine())
      ->withTransport(new TempFileTransport())
      ->withBinaryPath('/usr/bin/php')
      ->withRunnerPath(__DIR__ . '/vendor/flytachi/winter-thread/wRunner')
      ->withSecurity('your-secret-key')
);

ManualEngine ничего не настраивает за вас

transport, binaryPath и runnerPath должны быть заданы все, иначе engine выбросит ThreadException при обращении к ним. withSecurity() опционален. Withers-методы иммутабельны — каждый возвращает новый клон. Используйте ManualEngine, только если вам сознательно нужен полный контроль; иначе AdaptiveEngine сам обрабатывает распространённые случаи.

Или сохраните автоопределение AdaptiveEngine, но переопределите отдельные аспекты именованными аргументами:

php
<?php

use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Engine\AdaptiveEngine;
use Flytachi\Winter\Thread\Payload\TempFileTransport;

Thread::bindEngine(new AdaptiveEngine(
  secret: 'your-secret-key',
  transport: new TempFileTransport(),
  binaryPath: '/usr/bin/php',
));

AdaptiveEngine — это readonly, иммутабельный класс: чтобы изменить один аспект, создайте новый экземпляр. Полную поверхность engine см. в Справочнике API.

Секрет

Секрет включает HMAC-подпись сериализованного payload (через opis/closure), поэтому поддельный или изменённый payload отклоняется до того, как в дочернем процессе будет построен хоть один объект. Подпись включается по желанию, но настоятельно рекомендуется всегда, когда вы сериализуете замыкания или анонимные классы.

Задать его можно тремя способами — ManualEngine::withSecurity(), аргумент AdaptiveEngine(secret:) (показан выше) или переменная окружения WINTER_THREAD_SECRET, которую AdaptiveEngine читает автоматически:

bash
export WINTER_THREAD_SECRET='your-secret-key'

Секрет попадает в дочерний процесс через эту переменную окружения (доступный только владельцу /proc/<pid>/environ), а не через argv.

Задайте секрет в продакшене

Без секрета payload по-прежнему обрабатывается opis/closure (никогда штатным unserialize()), но он непроверенный — доверие сводится к приватному, доступному только владельцу каналу доставки. Задайте секрет, чтобы отклонять поддельные payload до того, как они смогут выполнить код в дочернем процессе.

Замечания об окружении

PHP-FPM / веб-SAPI

proc_open должен быть разрешён (не перечислен в disable_functions). Под FPM/CGI PHP_BINARY указывает на бинарник FPM, а не CLI — запускать через него ваш воркер было бы неправильно. Используемый по умолчанию AdaptiveEngine определяет не-CLI SAPI и вместо этого разрешает настоящий бинарник PHP CLI из PHP_BINDIR/php. Если определение не сработает в необычной конфигурации, укажите путь явно через withBinaryPath() у ManualEngine или аргумент AdaptiveEngine(binaryPath:).

Путь к runner

Дочерний процесс инициализируется скриптом wRunner, поставляемым в корне пакета. AdaptiveEngine находит его автоматически по пути vendor/flytachi/winter-thread/wRunner. Две ситуации требуют внимания:

  • Phar / перемещённые развёртывания. Если ваш код упакован в .phar или каталог vendor находится не на обычном пути файловой системы, скрипт может оказаться напрямую неисполняемым. Укажите ManualEngine на настоящую копию на диске через withRunnerPath(...).
  • open_basedir. Пути к бинарнику и runner должны находиться внутри любого настроенного open_basedir.

Контейнеры

Если вы запускаете detached-задачи, а ваше приложение является PID 1 в контейнере, добавьте init-процесс, собирающий зомби (docker run --init или init: true в Compose), чтобы осиротевшие воркеры собирались. Без этого detached-воркеры переподчиняются вашему приложению (PID 1), которое их не reap’ает, и они накапливаются как зомби. Attached-задачам, которые вы join()/reap(), это не нужно.

Проверьте установку

php
<?php
require 'vendor/autoload.php';

use Flytachi\Winter\Thread\Runnable;
use Flytachi\Winter\Thread\Thread;

$thread = new Thread(new class implements Runnable {
  public function run(array $args): void { /* ничего */ }
});

echo 'PID:  ' . $thread->start() . PHP_EOL;
echo 'exit: ' . $thread->join() . PHP_EOL; // 0

Ожидаемый вывод — числовой PID, за которым следует exit: 0. Анонимные классы работают, потому что opis/closure — жёсткая зависимость; вы не ограничены именованными классами задач (хотя именованные классы дают более читаемые заголовки процессов и упрощают отладку).

Далее пройдите реальную задачу в Быстром старте.