Пакет · thread

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

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

Привязка лаунчера при инициализации

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

Значение по умолчанию без конфигурации — AdaptiveLauncher: на каждый запуск он смотрит на рантайм и направляет вызов в SwooleLauncher (внутри корутины) или CliLauncher (обычный CLI, FPM), разрешая бинарник, путь wRunner и секрет за вас. Если вы ничего не привязываете, Thread::launcher() создаёт его лениво — так что привязка нужна только там, где требуется переопределение.

bootstrap.php
<?php

use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\AdaptiveLauncher;

// Явно то же самое, что происходит без конфигурации.
Thread::bindLauncher(AdaptiveLauncher::adaptive());

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

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\AdaptiveLauncher;

Thread::bindLauncher(AdaptiveLauncher::adaptive(
  secret:     $_ENV['APP_SECRET'],
  binaryPath: '/usr/bin/php',
));

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

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

Транспорт задаётся только у CliLauncher

У AdaptiveLauncher::adaptive() аргумента transport нет: под Swoole доставку выбирает SwooleLauncher сам (разделяемая память при ext-shmop, иначе временный файл). Явный транспорт имеет смысл на пути CLI/FPM — привязывайте тогда CliLauncher::adaptive(transport: …).

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

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

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\AdaptiveLauncher;

Thread::bindLauncher(AdaptiveLauncher::adaptive(binaryPath: '/usr/bin/php'));

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

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\CliLauncher;
use Flytachi\Winter\Thread\Payload\TempFileTransport;

Thread::bindLauncher(new CliLauncher(
  binaryPath: '/usr/bin/php',
  runnerPath: __DIR__ . '/vendor/flytachi/winter-thread/wRunner',
  transport:  new TempFileTransport(),
));

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

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

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

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

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\AdaptiveLauncher;

Thread::bindLauncher(AdaptiveLauncher::adaptive(
  runnerPath: __DIR__ . '/vendor/flytachi/winter-thread/wRunner',
));

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

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

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

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\AdaptiveLauncher;

// 1. Именованный аргумент лаунчера
Thread::bindLauncher(AdaptiveLauncher::adaptive(secret: $_ENV['APP_SECRET']));

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

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

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

Определять Swoole самостоятельно не нужно. Под активным рантаймом (внутри корутины или с включёнными runtime-хуками) AdaptiveLauncher направляет запуск в SwooleLauncher, а тот доставляет payload без канала — разделяемой памятью при ext-shmop, иначе временным файлом. Сырой канал stdin там не годится: он не корутино-безопасен.

Явно задать транспорт можно только на пути CLI/FPM, у CliLauncher:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\CliLauncher;
use Flytachi\Winter\Thread\Payload\ShmTransport;

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

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

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

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

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

$launcher = Thread::launcher();
$payload  = \Opis\Closure\serialize($runnable, $launcher->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.

Связанное