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 an Engine. 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 engine never has to serialize the transport choice.

Selecting a transport

A transport is chosen on an Engine, then the engine is bound process-wide once at bootstrap with Thread::bindEngine(). There is no per-Thread setting.

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Engine\AdaptiveEngine;
use Flytachi\Winter\Thread\Payload\TempFileTransport;

// Self-configuring engine, override just the transport:
Thread::bindEngine(new AdaptiveEngine(transport: new TempFileTransport()));

For an explicit clean-slate engine, use ManualEngine and wither methods:

php
use Flytachi\Winter\Thread\Thread;
use Flytachi\Winter\Thread\Engine\ManualEngine;
use Flytachi\Winter\Thread\Payload\ShmTransport;

Thread::bindEngine(
  (new ManualEngine())
      ->withTransport(new ShmTransport())
      ->withBinaryPath(PHP_BINARY)
      ->withRunnerPath('/path/to/wRunner')
);

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

  • You run under Swoole — the AdaptiveEngine already switches to TempFileTransport automatically (below); override only if you specifically want ShmTransport.
  • 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

The default AdaptiveEngine self-configures its transport. When no transport is passed, 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.