Package · thread · reference

Payload transports

How the serialized Runnable is delivered from the parent to the child process. Delivery is a pluggable transport object implementing PayloadTransport, selected on the launcher. The default (PipeTransport) is fine for plain CLI and PHP-FPM; the other two create zero parent-side pipe descriptors, which is what makes them safe under event loops like Swoole. Why that matters: Swoole & payload delivery.

The three transports

All three deliver the exact same bytes — your task code never knows which was used. They differ only in mechanism and prerequisites.

Transport Delivery Parent pipe fd Swoole-safe Requires
PipeTransport serialized task written to the child’s stdin pipe after launch yes no
TempFileTransport task written to a 0600 temp file placed on stdin, unlinked right after launch none yes writable sys_get_temp_dir()
ShmTransport task placed in System V shared memory (0600), key passed via --shmkey none yes ext-shmop
  • PipeTransport — the default in plain CLI: simplest, nothing on disk, no extra extension. The parent writes the payload into the pipe after proc_open and closes it; the child reads its STDIN to EOF. The pipe file descriptor is why it is not Swoole-safe.
  • TempFileTransport — avoids pipe descriptors entirely. The file is created 0600 (owner-only) in the system temp dir and unlinked immediately after the process starts — the child keeps its open fd, so nothing lingers on disk even though the path is gone. No extension needed. Throws ThreadException if the temp file can’t be created or written.
  • ShmTransport — also avoids pipes, keeping the payload in RAM only. The parent allocates a 0600 segment, writes the payload, and passes the integer key as --shmkey; the child reads the segment and deletes it. The child’s stdin is /dev/null. Requires ext-shmop — throws ThreadException (ShmTransport requires ext-shmop.) on both the staging and receiving side if the extension is missing, and on allocation failure.

How the child chooses its receiving side

The parent’s chosen transport determines staging, but the child picks how to receive purely from CLI options: if --shmkey is present it reads shared memory, otherwise it reads STDIN. Because Pipe and TempFile both arrive on STDIN, the child treats them identically — the launcher never has to serialize the transport choice.

Selecting a transport

A transport is set on a CliLauncher, which is bound process-wide once at bootstrap with Thread::bindLauncher(). There is no per-Thread setting.

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

// Self-configuring launcher, override just the transport:
Thread::bindLauncher(CliLauncher::adaptive(transport: new TempFileTransport()));

Or build the launcher outright, setting every part:

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

Thread::bindLauncher(new CliLauncher(
  binaryPath: PHP_BINARY,
  runnerPath: '/path/to/wRunner',
  transport:  new ShmTransport(),
));

This choice does not apply under Swoole

The transport belongs to CliLauncher — the CLI and FPM path. With an active Swoole runtime the launch goes to SwooleLauncher, which picks delivery itself (shared memory with ext-shmop, a temp file otherwise) and takes no transport from outside.

In normal CLI you rarely need to touch this — the default PipeTransport is fine. Pick a different one when:

  • You run under Swoole — the choice is already made for you (see the callout above and the section below); there is no transport to set here.
  • You want nothing on disk, ever — use ShmTransport (RAM only, needs ext-shmop).
  • Your temp dir is unusual (tiny tmpfs, noexec, restricted open_basedir) — Pipe or Shm avoid the temp file.

Automatic selection under Swoole

CliLauncher resolves the transport itself when one is not given. Left null, it uses TempFileTransport under an active Swoole runtime — detected inside a coroutine (\Swoole\Coroutine::getCid() !== -1) or with runtime hooks enabled (\Swoole\Runtime::getHookFlags() !== 0) — and PipeTransport otherwise (including when the Swoole extension is not loaded).

php
// Under an active Swoole runtime, this transparently uses TempFile:
$thread = new Thread(new MyTask());
$thread->start();

Merely having the extension installed but dormant does not force TempFile — the switch keys on an active runtime. The full fd-leak cascade and the output-pipe caveat live in Swoole & payload delivery.

Mechanics to know

  • TempFileTransport — the child holds the file descriptor open; the directory entry is removed immediately after launch, so nothing lingers on disk.
  • ShmTransport — stdin is /dev/null; the child reads the segment and deletes it, and the parent’s cleanup() deletes any survivor if the child crashed before reading. Keys are unique per start().
  • Parent-side cleanup is a fallback. A transport’s cleanup() runs when the handle finishes to release a temp file or shm segment the child never consumed. It is always safe to call, even when the resource is already gone.

Writing your own transport

Implement PayloadTransportstage / receive / cleanup — and bind it via an engine. Mind the child side: the default AdaptiveRunner receives from STDIN (or shm when --shmkey is present) and does not call a custom transport’s receive(). Delivering on the child’s stdin works transparently; an out-of-band channel (Redis, TCP, a FIFO) needs a matching child runner shipped alongside it. See Runner lifecycle.