Пакет · thread

API Reference

Полный справочник по каждому публичному типу в Flytachi\Winter\Thread. Сигнатуры точно соответствуют исходному коду; для каждого элемента перечислены параметры, возвращаемые значения, исключения и неочевидная семантика.

API с высоты птичьего полёта

Поверхность делится на два уровня. Большинству приложений нужен только публичный API — три типа. Низкоуровневый API нужен для построения собственного пула, планировщика или супервизора прямо на сырых примитивах.

Публичный API — то, что вы используете

Тип Вид Для чего используется
Runnable интерфейс описать задачу — поместите свою логику в run()
Thread класс запустить и управлять одной задачей как фоновым процессом
Launcher интерфейс как порождается процесс; привязывается один раз при инициализации
AdaptiveLauncher · CliLauncher · SwooleLauncher класс три бэкенда — маршрутизирующий, CLI/FPM, корутинный

Низкоуровневый и расширяющий API — то, на чём вы строите

Тип Вид Роль
ProcessHandle интерфейс управлять одним процессом (reap/join/detach/сигнал/чтение)
LaunchSpec DTO все параметры запуска в одном объекте-значении
PayloadTransport · Pipe / TempFile / Shm интерфейс / класс стратегия доставки payload
StagedPayload DTO результат подготовки, который потребляет launcher
Runner · AdaptiveRunner интерфейс / класс исполнение на стороне потомка
Signal класс POSIX-хелперы по сырому PID (учитывают зомби)
ThreadException класс единственное исключение библиотеки

Четыре интерфейса (Launcher, ProcessHandle, Runner, PayloadTransport) — это точки расширения, принимающие вашу собственную реализацию; всё остальное — конкретные типы, которые вы потребляете.


Публичный API

Runnable (interface)

Flytachi\Winter\Thread\Runnable — контракт задачи.

php
public function run(array $args): void

Поместите свою логику в run(); она выполняется в рабочем процессе.

Параметр Тип Описание
$args array<string, string|bool> значения конкретного запуска из start() — флаги приходят как булево true, все остальные значения как строки

Возвращает ничего. Сигнализируйте о результате через код возврата (return/нормальное завершение → 0; выброс исключения → ненулевой) или побочные эффекты (файл/БД/очередь).

Контракт: реализующий объект должен быть сериализуемым — никаких живых ресурсов (PDO, сокеты, потоки) в его свойствах; открывайте их внутри run(). Непойманное исключение перехватывается runner’ом, логируется в STDERR со стеком трассировки и превращается в ненулевой код возврата.


Thread (final class)

Flytachi\Winter\Thread\Thread — высокоуровневый фасад над одним процессом. Изменяемый; защищает от конкурентных запусков.

__construct

php
new Thread(
  Runnable $runnable,
  string   $namespace = '',
  ?string  $name = null,
  ?string  $tag = null,
)
Параметр Тип По умолчанию Описание
$runnable Runnable задача для выполнения
$namespace string '' логическая группировка, отображается в заголовке процесса ОС
$name ?string null имя задачи; при null — короткое имя класса Runnable (или 'anonymous' для анонимных классов)
$tag ?string null различитель экземпляра; показывается как @tag (по умолчанию @runnable в заголовке)

Конструирование ничего не запускает. Заголовок процесса — WinterThread <namespace> -> <name>@<tag> (только там, где существует cli_set_process_title()).

Статические — привязка лаунчера

Метод Возвращает Описание
Thread::bindLauncher(Launcher $launcher) void задать лаунчер на весь процесс (вызвать один раз при инициализации)
Thread::launcher() Launcher текущий лаунчер; лениво создаёт AdaptiveLauncher, если ни один не привязан

start

php
start(
  array   $arguments = [],
  bool    $debugMode = false,
  ?string $outputTarget = '/dev/null',
  bool    $detached = false,
): int

Сериализует задачу, запускает процесс и немедленно возвращает управление.

Параметр Тип По умолчанию Описание
$arguments array<string, scalar|null> [] аргументы конкретного запуска, доступные в $args внутри run(). true → флаг без значения; false/null → отбрасывается; прочие скаляры → строки; нескаляры игнорируются
$debugMode bool false включить E_ALL + display_errors в потомке
$outputTarget ?string '/dev/null' '/dev/null' отбрасывает (без pipe); путь дозаписывает (режим a); null направляет в pipe родителю для readOutput()/readError()
$detached bool false демонизировать (fork+setsid) для fire-and-forget без зомби

Возвращает int — PID запущенного процесса (в detached-режиме — эфемерный PID launcher’а). Выбрасывает ThreadException, если поток уже жив или процесс не удаётся запустить.

Чтение вывода из pipe

Передача null направляет stdout/stderr в pipe родителю. join() и reap() вычитывают эти pipe’ы, пока ждут, поэтому голый join() не зависает, а readOutput() после него возвращает весь вывод. Broken pipe случается только в настоящем fire-and-forget — вы передали null и потом ни разу не читаете, не вызываете join() или reap(); для этого случая оставляйте значение по умолчанию '/dev/null'. См. Вывод и Broken Pipe.

Жизненный цикл и ожидание

Метод Возвращает Описание
join(int $timeout = 0) ?int блокирует (опрос каждые 50 мс) до завершения, вычитывая pipe’ы stdout/stderr, пока ждёт; возвращает код возврата, null по таймауту (секунды; 0 = бесконечно) или -1, если не был запущен или воркер убит сигналом. Пожинает по завершении
reap() bool неблокирующий: вычитывает pipe’ы, затем true, если завершён/отсутствует (и пожат, код возврата установлен), false, если ещё выполняется
detach() void прекратить отслеживание (неблокирующий; без proc_close). После этого isAlive()false, reap()true, сигналы → false, код возврата никогда не собирается. Внимание: detach() ≠ detached-режим

Состояние

Метод Возвращает Описание
getPid() ?int PID запущенного процесса или null до start(); PID launcher’а в detached-режиме
isAlive() bool выполняется ли процесс сейчас? false до запуска / после завершения / после detach()
getExitCode() ?int код возврата после пожинания (join/reap); -1, если воркер убит сигналом; null до пожинания и null навсегда после detach()

Сигналы (требуют ext-posix; каждый возвращает false, если не выполняется)

Метод Сигнал Описание
pause() SIGSTOP приостановить (неблокируемый)
resume() SIGCONT возобновить приостановленный воркер
interrupt() SIGINT эквивалент Ctrl+C (перехватываемый)
terminate() SIGTERM запрос корректной остановки (перехватываемый)
kill() SIGKILL принудительное убийство (неблокируемое)

Каждый возвращает booltrue, если сигнал был отправлен. Чтобы отреагировать корректно, задача должна установить обработчик; см. Корректное завершение.

Вывод (непустой только при запуске с outputTarget: null)

Метод Возвращает Описание
readOutput() string STDOUT, пришедший с момента последнего вызова (потребляющий, неблокирующий); '', если ничего / не в pipe. Буферизуется join()/reap(), поэтому возвращает весь вывод даже после завершения процесса
readError() string STDERR, пришедший с момента последнего вызова (потребляющий, неблокирующий); '', если ничего / не в pipe

Метаданные

Метод Возвращает Описание
getNamespace() string пространство имён
getName() string разрешённое имя задачи
getTag() ?string тег или null

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

Конфигурация запуска живёт в Launcher — он описан ниже, в разделе низкоуровневого API, потому что это же расширяемая точка. Привязывается один раз при инициализации:

php
Thread::bindLauncher(CliLauncher::adaptive(secret: ''));

Thread::launcher();   // текущий лаунчер; без привязки — AdaptiveLauncher

Низкоуровневый и расширяющий API

Для пулов, планировщиков и собственных бэкендов. Thread построен ровно на этих частях — вы можете управлять ими напрямую.

Launcher (interface)

Flytachi\Winter\Thread\Launch\Launcher — как порождается процесс. Живёт только на стороне родителя: потомку он не передаётся, там работает отдельный AdaptiveRunner, а секрет доезжает через переменную окружения WINTER_THREAD_SECRET.

php
interface Launcher
{
  public function launch(LaunchSpec $spec): ProcessHandle;
  public function security(): ?DefaultSecurityProvider;
}
Метод Возвращает Описание
launch($spec) ProcessHandle породить процесс и вернуть дескриптор управления; выбрасывает ThreadException при сбое
security() ?DefaultSecurityProvider провайдер подписи payload или null, если секрета нет

Вся поверхность — два метода: транспорт, путь к бинарнику и путь к wRunner в интерфейс не входят, это внутреннее дело конкретного бэкенда. Поэтому свой запуск (SSH, контейнер, удалённый узел) реализует ровно то, что ему нужно.

AdaptiveLauncher (final readonly class)

Flytachi\Winter\Thread\Launch\AdaptiveLauncher — используется, если вы ничего не привязали. Держит оба встроенных бэкенда и выбирает один на каждый launch() по рантайму в этот момент: внутри корутины Swoole или при включённых хуках — SwooleLauncher, иначе — CliLauncher.

php
new AdaptiveLauncher(CliLauncher $cli, SwooleLauncher $swoole)

AdaptiveLauncher::adaptive(
  ?string $secret = null,
  ?string $binaryPath = null,
  ?string $runnerPath = null,
): AdaptiveLauncher

CliLauncher (final readonly class)

Flytachi\Winter\Thread\Launch\CliLauncher — бэкенд для CLI и FPM, на основе proc_open. Команда собирается полностью экранированной через escapeshellarg.

php
new CliLauncher(
  string            $binaryPath,
  string            $runnerPath,
  ?PayloadTransport $transport = null,
  ?string           $secret = null,
)

CliLauncher::adaptive(
  ?string           $secret = null,
  ?PayloadTransport $transport = null,
  ?string           $binaryPath = null,
  ?string           $runnerPath = null,
): CliLauncher
Параметр Что подставляется при null
$secret переменная окружения WINTER_THREAD_SECRET, иначе null (без подписи)
$transport не резолвится при сборке — выбирается на каждом launch(): TempFileTransport при активном рантайме Swoole, иначе PipeTransport
$binaryPath CLI SAPI → PHP_BINARY; не-CLI (FPM/CGI) → PHP_BINDIR/php, если исполняемый, иначе 'php'
$runnerPath упакованный wRunner

Неизменяемый — чтобы изменить одну часть, соберите новый экземпляр.

SwooleLauncher (final readonly class)

Flytachi\Winter\Thread\Launch\SwooleLauncher — бэкенд для активного рантайма Swoole. Вместо proc_open (портит дескрипторы реактора) и Swoole\Process (запрещён при поднятых async-io потоках) запускает runner фоновой задачей оболочки через Swoole\Coroutine\System::exec().

php
new SwooleLauncher(string $binaryPath, string $runnerPath, ?string $secret = null)

SwooleLauncher::adaptive(
  ?string $secret = null,
  ?string $binaryPath = null,
  ?string $runnerPath = null,
): SwooleLauncher

Транспорта в сигнатуре нет: доставку он выбирает сам — разделяемая память при ext-shmop, иначе временный файл. Требует ext-swoole в момент запуска и осмыслен только для detached-задач: у корутинного воркера нет родительского пайпа, поэтому его дескриптор работает по PID — живость и сигналы есть, читать и join() нечего.


ProcessHandle (interface)

Flytachi\Winter\Thread\Launch\ProcessHandle — низкоуровневый примитив управления процессом, который возвращает лаунчер. Не конструируется напрямую — получается из Launcher::launch().

Это интерфейс: свой бэкенд возвращает собственную реализацию. Встроенных две — CliProcessHandle (оборачивает ресурс proc_open, отслеживает состояние процесса, pipe’ов и кода возврата) и SwooleProcessHandle (работает по PID: живость и сигналы есть, читать и join() нечего).

Метод Возвращает Описание
getPid() int PID процесса
isAlive() bool выполняется ли процесс сейчас?
join(int $timeout = 0) ?int блокирует до завершения, вычитывая stdout/stderr, пока ждёт; код возврата, null по таймауту (секунды) или записанный код / -1, если ресурс уже пропал
reap() bool неблокирующий; вычитывает stdout/stderr; true, если завершён (и пожат)
detach() void прекратить отслеживание; закрывает pipe’ы, без proc_close, без очистки транспорта (ей владеет потомок)
getExitCode() ?int код возврата после пожинания; null после detach()
readOutput() string потребляющий STDOUT (байты с момента последнего вызова, неблокирующий); '', если нет pipe вывода
readError() string потребляющий STDERR (байты с момента последнего вызова, неблокирующий); '', если нет pipe вывода
signal(int $signal) bool posix_kill(pid, signal) — только если жив; иначе false

reap(), detach() и __destruct() неблокирующие на живом процессе; блокирующий proc_close выполняется только на уже мёртвом процессе. __destruct() пожинает завершённый процесс либо отсоединяет ещё работающий — он никогда не блокирует родителя.


LaunchSpec (final readonly class)

Flytachi\Winter\Thread\LaunchSpec — неизменяемый набор всех параметров запуска. Постройте его напрямую, чтобы управлять launcher’ом без Thread.

php
new LaunchSpec(
  string  $payload,
  string  $namespace = '',
  string  $name = 'anonymous',
  ?string $tag = null,
  array   $arguments = [],
  bool    $debug = false,
  ?string $output = '/dev/null',
  bool    $detached = false,
)
Параметр Тип По умолчанию Описание
$payload string уже сериализованный Runnable
$namespace string '' группировка для заголовка процесса
$name string 'anonymous' имя для заголовка процесса
$tag ?string null тег экземпляра
$arguments array<string, scalar|null> [] передаётся задаче как --arg-*
$debug bool false отчёт об ошибках на стороне потомка
$output ?string '/dev/null' '/dev/null' | путь к файлу | null (pipe родителю)
$detached bool false демонизировать потомка

Все свойства публичные и readonly.


PayloadTransport (interface)

Flytachi\Winter\Thread\Payload\PayloadTransport — доставляет сериализованный payload через границу процессов в две части плюс очистка.

Метод Возвращает Выполняется в Описание
stage(string $payload) StagedPayload родитель подготовить доставку (спецификация fd-0, аргументы CLI, ссылка для очистки)
receive(array $options) string потомок прочитать payload обратно (STDIN или shm через shmkey)
cleanup(StagedPayload $staged) void родитель освободить подготовленные ресурсы; безопасно, даже если они уже пропали

$options в receive() — это array<string, mixed> — разобранные опции CLI потомка.

Кастомным транспортам нужен подходящий runner

Стандартный AdaptiveRunner принимает только из STDIN или shm — он не вызывает receive() кастомного транспорта. Поэтому транспорт, доставляющий вне полосы (Redis/TCP/FIFO), также требует подходящего runner’а на стороне потомка. См. Swoole и доставка payload.

PipeTransport

…\Payload\PipeTransport — payload через pipe stdin потомка (записывается после запуска). Без расширений. Не безопасен для Swoole (использует pipe fd). По умолчанию в обычном CLI.

TempFileTransport

…\Payload\TempFileTransport — payload через временный файл 0600, помещённый на stdin, удаляемый (unlink) сразу после запуска (потомок сохраняет свой fd). Без расширений. Безопасен для Swoole. Выбрасывает ThreadException, если временный файл не удаётся создать/записать.

ShmTransport

…\Payload\ShmTransport — payload через сегмент System V разделяемой памяти 0600; целочисленный ключ передаётся как --shmkey, а потомок читает, затем удаляет его. Без диска, безопасен для Swoole. Требует ext-shmop — выбрасывает ThreadException ("ShmTransport requires ext-shmop.") как в stage(), так и в receive(), если расширения нет, а также при сбое выделения.

StagedPayload (final readonly class)

Flytachi\Winter\Thread\Payload\StagedPayload — результат stage(); launcher читает его обобщённо.

php
new StagedPayload(
  array   $stdinSpec,
  array   $cliArgs = [],
  ?string $pipePayload = null,
  ?string $unlinkAfterOpen = null,
  mixed   $ref = null,
)
Параметр Тип По умолчанию Описание
$stdinSpec array<int,string> дескриптор fd-0 для proc_open (['pipe','r'] или ['file',$path,'r'])
$cliArgs array<int,string> [] дополнительные, уже безопасные аргументы запуска (например, ['--shmkey=123'])
$pipePayload ?string null записывается в pipe после запуска (транспорт pipe)
$unlinkAfterOpen ?string null удаляется (unlink) после запуска, когда потомок держит свой fd (транспорт temp-file)
$ref mixed null непрозрачный дескриптор очистки (путь temp / ключ shm), передаваемый в cleanup()

Runner (interface)

Flytachi\Winter\Thread\Runner\Runner — стратегия исполнения на стороне потомка.

php
public function execute(array $options): int
Параметр Тип Описание
$options array<string, mixed> разобранные опции CLI wRunner (namespace, name, tag, debug, detach, shmkey)

Возвращает int — код возврата процесса (0 — успех, ненулевой — сбой).

AdaptiveRunner (final readonly class)

Flytachi\Winter\Thread\Runner\AdaptiveRunner — стандартный runner на стороне потомка (управляется wRunner). Он зависит только от провайдера безопасности — не от лаунчера — так что обе стороны остаются независимыми.

php
new AdaptiveRunner(
  ?DefaultSecurityProvider $security = null,
  mixed                    $errStream = null,
)
Параметр Тип По умолчанию Описание
$security ?DefaultSecurityProvider null проверяет подпись payload; постройте его из того же секрета, которым подписал родитель (null = без подписи). wRunner строит его из WINTER_THREAD_SECRET
$errStream resource|null null куда пишутся диагностические сообщения; по умолчанию STDERR (внедряемо для тестов)

Ход execute(): принять payload (--shmkey → shm, иначе STDIN) → проверить + десериализовать через opis/closure (отклоняя пустые, не-Runnable или неподписанные/подделанные payload’ы ненулевым кодом возврата) → опционально fork+setsid (detached) → установить заголовок процесса → run() → код возврата.

Замена runner'а

Чтобы использовать свой Runner, вы заменяете bootstrap потомка: напишите собственный скрипт вроде wRunner, который конструирует ваш runner (обычно запускается кастомным Launcher). Для runner’а нет шва вроде bindRunner() — по замыслу сторона потомка независима от лаунчера родителя. См. Жизненный цикл runner’а.


Signal (final class)

Flytachi\Winter\Thread\Signal — статические POSIX-хелперы по сырому PID. isProcessRunning() трактует зомби (состояние Z) как не выполняющиеся, как на Linux (/proc/<pid>/status), так и на macOS (ps).

Метод Возвращает Описание
isProcessRunning(int $pid) bool существует ли живой (не-зомби) процесс с этим PID?
interrupt(int $pid) bool отправить SIGINT (если выполняется)
termination(int $pid) bool отправить SIGTERM (если выполняется)
close(int $pid) bool отправить SIGHUP (если выполняется)
kill(int $pid) bool отправить SIGKILL (если выполняется)
wait(int $pid, int $timeout = 10) bool опрашивать, пока не пропадёт; false по таймауту (секунды)
interruptAndWait(int $pid, int $timeout = 10) bool SIGINT, затем ожидание
terminationAndWait(int $pid, int $timeout = 10) bool SIGTERM, затем ожидание
closeAndWait(int $pid, int $timeout = 10) bool SIGHUP, затем ожидание

Нет pause/resume в Signal

Signal покрывает только сигналы остановки/завершения. Приостановка/возобновление по сырому PID не предоставляются — используйте posix_kill($pid, SIGSTOP) / posix_kill($pid, SIGCONT) напрямую либо API Thread::pause()/resume().

PID reuse

Сырые PID подвержены переиспользованию PID (PID reuse) в ОС — PID может быть переназначен после завершения исходного процесса. Предпочитайте Thread/ProcessHandle для надёжного отслеживания; используйте Signal только со свежеполученными PID (например, самостоятельно сообщённый PID detached-воркера). См. PID reuse и сигналы.


ThreadException (class)

Flytachi\Winter\Thread\ThreadException extends \RuntimeException — единственный тип исключения библиотеки.

Выбрасывается при: сбое запуска (proc_open отклонён или процесс сразу умер), запуске уже работающего Thread, некорректной конфигурации (например, ShmTransport без ext-shmop) и сбоях подготовки транспорта (временный файл / выделение shm). Ловите его вокруг start() и настройки лаунчера.