Установка и требования
Winter Thread рассчитан на POSIX-окружение PHP CLI. Установите его одной командой Composer, убедитесь, что присутствует пара расширений, — и для большинства приложений не пишите вообще никакой конфигурации.
Установка
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:
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
Конфигурация действует на весь процесс и живёт в Launcher — объекте на стороне родителя, который запускает процесс и владеет секретом подписи payload. Конкретный лаунчер знает ещё и про бинарник PHP, путь к runner-скрипту и транспорт payload, но это уже его внутреннее дело. Привяжите лаунчер один раз во время bootstrap вашего приложения.
Без конфигурации (рекомендуется)
Для большинства приложений настраивать нечего. Если вы ничего не привязываете, первый
Thread лениво создаёт AdaptiveLauncher, который на каждый запуск выбирает бэкенд под
текущий рантайм: внутри корутины Swoole — SwooleLauncher, в обычном CLI и под FPM —
CliLauncher. Просто запускайте потоки:
<?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();Переопределить отдельные части
Чтобы задать что-то своё, оставив остальное на автоопределении, соберите CliLauncher::adaptive()
с именованными аргументами и привяжите его через Thread::bindLauncher():
<?php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Launch\CliLauncher;
use Flytachi\Winter\Thread\Payload\TempFileTransport;
Thread::bindLauncher(CliLauncher::adaptive(
secret: 'your-secret-key',
transport: new TempFileTransport(),
));Незаданные аргументы резолвятся сами: binaryPath — из текущего SAPI, runnerPath — из
установленного пакета, secret — из WINTER_THREAD_SECRET.
Задать всё явно
Полностью воспроизводимая сборка, без обращений к окружению — то, что нужно в контейнере:
<?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(), // null — определять на каждый запуск
secret: 'your-secret-key', // null — без подписи
));Транспорт определяется на запуске, а не при привязке
Если transport оставлен null, он выбирается внутри каждого launch(): при активном рантайме
Swoole — TempFileTransport, иначе PipeTransport. Это сделано намеренно: лаунчер, привязанный
на этапе preload (когда воркеров и корутин ещё нет), иначе зафиксировал бы неверный выбор.
CliLauncher — final readonly: чтобы поменять одну часть, соберите новый экземпляр.
Полную поверхность см. в Справочнике API.
Голый CliLauncher — только вне корутины
CliLauncher использует proc_open, который портит дескрипторы реактора при вызове изнутри
живой корутины Swoole — второй запуск падает с Bad file descriptor. Обычно вы этого не
видите: AdaptiveLauncher по умолчанию направляет туда SwooleLauncher. Привязывайте голый
CliLauncher только для CLI/FPM.
Секрет
Секрет включает HMAC-подпись сериализованного payload (через opis/closure), поэтому поддельный или
изменённый payload отклоняется до того, как в дочернем процессе будет построен хоть один объект.
Подпись включается по желанию, но настоятельно рекомендуется всегда, когда вы сериализуете
замыкания или анонимные классы.
Задать его можно двумя способами — аргументом secret: у лаунчера (показан выше) или
переменной окружения WINTER_THREAD_SECRET, которую лаунчер читает сам, если аргумент не
передан. Порядок именно такой: явный аргумент, затем переменная окружения, иначе подписи нет.
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 — запускать через него ваш воркер было бы неправильно.
CliLauncher это учитывает: под CLI-SAPI он берёт PHP_BINARY, а под не-CLI разрешает настоящий
бинарник PHP CLI из PHP_BINDIR/php (если тот исполняем), иначе откатывается на php из PATH.
Если определение не сработает в необычной конфигурации, укажите путь явно аргументом
binaryPath:.
Путь к runner
Дочерний процесс инициализируется скриптом wRunner, поставляемым в корне пакета. Лаунчер
находит его автоматически по пути vendor/flytachi/winter-thread/wRunner. Две ситуации требуют
внимания:
- Phar / перемещённые развёртывания. Если ваш код упакован в
.pharили каталог vendor находится не на обычном пути файловой системы, скрипт может оказаться напрямую неисполняемым. Укажите настоящую копию на диске аргументомrunnerPath:. open_basedir. Пути к бинарнику и runner должны находиться внутри любого настроенногоopen_basedir.
Контейнеры
Если вы запускаете detached-задачи, а ваше приложение является PID 1 в контейнере, добавьте
init-процесс, собирающий зомби (docker run --init или init: true в Compose), чтобы осиротевшие
воркеры собирались. Без этого detached-воркеры переподчиняются вашему приложению (PID 1), которое их
не reap’ает, и они накапливаются как зомби. Attached-задачам, которые вы join()/reap(), это не
нужно.
Проверьте установку
<?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 — жёсткая зависимость; вы не ограничены именованными классами задач (хотя именованные
классы дают более читаемые заголовки процессов и упрощают отладку).
Именованный класс задачи должен быть доступен автозагрузчику
Анонимный класс уезжает в дочерний процесс целиком, а именованный — только по имени: дочерний
процесс загружает его сам через vendor/autoload.php. Класс, объявленный прямо в скрипте (или
вне автозагрузки), там не найдётся, и задача упадёт с exit: 1. Причину видно, если направить
вывод в файл вместо /dev/null:
Error: failed to deserialize payload: Class "OnlyHereTask" does not existПоэтому классы задач держат там же, где остальной код приложения — под автозагрузкой PSR-4.
Далее пройдите реальную задачу в Быстром старте.