Пакет · thread

Интеграция с фреймворком

Winter Thread порождает по одному PHP CLI-процессу на задачу. Внутри фреймворка или веб-SAPI обычно нужно указать ему, какой PHP запускать, где находится дочерний bootstrap и как подписывать payload’ы. Всё это живёт в одном Engine, который вы привязываете один раз при старте приложения.

Привязка engine при инициализации

Конфигурация проходит через единственную точку: Thread::bindEngine(). Вызовите её один раз, рано — в boot() сервис-провайдера, в bootstrap контейнера или в любом файле, который загружает каждый процесс (включая воркеры Swoole). Каждый последующий Thread::start() использует привязанный engine.

Значение по умолчанию без конфигурации — AdaptiveEngine: он определяет окружение и разрешает transport, PHP-бинарник, путь wRunner и секрет за вас. Если вы ничего не привязываете, Thread::engine() создаёт его лениво — так что привязывать нужно только тогда, когда требуется переопределение.

bootstrap.php
<?php

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

// Без конфигурации: определяет бинарник, transport и секрет из окружения.
Thread::bindEngine(new AdaptiveEngine());

Чтобы переопределить лишь один аспект, передайте именованный аргумент — AdaptiveEngine неизменяем, поэтому создайте новый экземпляр:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Engine\AdaptiveEngine;

Thread::bindEngine(new AdaptiveEngine(
  secret:     $_ENV['APP_SECRET'],
  binaryPath: '/usr/bin/php',
));

bindEngine — единственная привязка

Thread::bindEngine() заменяет старые привязчики отдельных настроек. Больше нет ни bindBinaryPath(), ни bindRunner(), ни bindSerSecurity(), ни bindPayloadMode() — каждая из них теперь является свойством engine. Повторная привязка свежего engine — это и есть способ «сбросить» настройки к значениям по умолчанию.

Задание PHP-бинарника под FPM

Под PHP-FPM или CGI запущенный интерпретатор — это веб-обработчик, а не CLI-бинарник, который может порождать фоновые воркеры. AdaptiveEngine уже справляется с этим: под не-CLI SAPI он разрешает PHP_BINDIR/php (с откатом на php из PATH), поэтому обычный случай не требует конфигурации. Переопределяйте его только когда CLI-бинарник находится в другом месте:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Engine\AdaptiveEngine;

Thread::bindEngine(new AdaptiveEngine(binaryPath: '/usr/bin/php'));

Для явной, независимой от окружения конфигурации используйте ManualEngine — он ничего не определяет и требует задать каждую часть:

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

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

Признак неверного бинарника

Если задачи молча не запускаются из веб-запроса, обычная причина — неверный путь к бинарнику: дочерний процесс был запущен под FPM SAPI вместо CLI. Сначала проверьте binaryPath().

Указание на упакованный скрипт runner

Дочерний процесс инициализируется упакованным скриптом wRunner (не wExecutor — этого имени больше нет). AdaptiveEngine находит его автоматически. Если ваш деплой перемещает vendor/ или вы поставляете собственный bootstrap, задайте путь явно:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Engine\AdaptiveEngine;

Thread::bindEngine(new AdaptiveEngine(
  runnerPath: __DIR__ . '/vendor/flytachi/winter-thread/wRunner',
));

Собственный bootstrap runner — это забота дочерней стороны, независимая от engine; если вы его заменяете, то обычно заменяете и launcher. См. Жизненный цикл runner.

Подпись payload’ов для всего приложения

opis/closure сериализует и (опционально) подписывает задачи, чтобы замыкания и анонимные классы могли пересекать границу процесса. Задайте секрет для всего приложения — и поддельные или изменённые payload’ы будут отклонены в дочернем процессе ещё до создания любого объекта. Три эквивалентных способа задать его:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Engine\AdaptiveEngine;
use Flytachi\Winter\Thread\Engine\ManualEngine;

// 1. Именованный аргумент AdaptiveEngine
Thread::bindEngine(new AdaptiveEngine(secret: $_ENV['APP_SECRET']));

// 2. Wither ManualEngine
Thread::bindEngine((new ManualEngine())->withSecurity($_ENV['APP_SECRET']));

// 3. Вообще без кода — задайте переменную окружения WINTER_THREAD_SECRET; AdaptiveEngine прочитает её.

Секрет достигает дочернего процесса через переменную окружения WINTER_THREAD_SECRET (доступную только владельцу), никогда через argv — поэтому подпись продолжает работать даже для ManualEngine с явным секретом. Подробности в Безопасность и производительность.

Swoole: transport выбирается автоматически

Больше не нужно определять Swoole самостоятельно. AdaptiveEngine выбирает TempFileTransport автоматически всякий раз, когда работает под активным рантаймом Swoole (внутри корутины или с включёнными runtime-хуками) — потому что сырой канал stdin не является корутино-безопасным. Вне Swoole он использует PipeTransport. Старый сниппет с ручным определением Swoole больше не нужен.

При желании вы всё ещё можете принудительно задать конкретный transport:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Engine\AdaptiveEngine;
use Flytachi\Winter\Thread\Payload\ShmTransport;

// Принудительная доставка через разделяемую память (требует ext-shmop).
Thread::bindEngine(new AdaptiveEngine(transport: new ShmTransport()));

Компромиссы transport’ов описаны в Swoole и доставка payload и Режимы payload.

Продвинутое: построение пула воркеров без фасада Thread

Код фреймворка, управляющий собственным пулом, может обратиться к launcher и управлять им напрямую — по одному ProcessHandle на воркер вместо объекта Thread на задачу. Сериализуйте задачу с помощью провайдера безопасности engine, постройте LaunchSpec и собирайте результаты неблокирующим циклом:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\LaunchSpec;

$launcher = Thread::engine()->launcher();
$payload  = \Opis\Closure\serialize($runnable, Thread::engine()->security());

$handle = $launcher->launch(new LaunchSpec(
  payload:   $payload,
  namespace: 'Pool',
  name:      'MyTask',
));

// неблокирующе: true, когда завершён (и реапнут), false пока ещё выполняется
if ($handle->reap()) {
  $exit = $handle->getExitCode();
}

Поскольку reap() и detach() никогда не блокируются на живом процессе, один цикл может управлять сотнями handle без зомби. Полные сигнатуры LaunchSpec, Launcher и ProcessHandle — в справочнике API.

Связанное