Установка и требования
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
Конфигурация действует на весь процесс и живёт в Engine — объекте-стратегии на стороне родителя,
который решает, какой бинарник PHP, runner-скрипт и транспорт payload использует Thread. Привяжите
его один раз во время bootstrap вашего приложения.
Без конфигурации (рекомендуется)
Для большинства приложений настраивать нечего. Если вы никогда не привязываете engine, первый
Thread лениво создаёт AdaptiveEngine по умолчанию, который самонастраивается под найденный
рантайм — обычный CLI, PHP-FPM/CGI или активный рантайм Swoole. Просто запускайте потоки:
<?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
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
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 читает
автоматически:
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
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 — жёсткая зависимость; вы не ограничены именованными классами задач (хотя именованные
классы дают более читаемые заголовки процессов и упрощают отладку).
Далее пройдите реальную задачу в Быстром старте.