Mental model
One idea explains almost every rule in this library: a “thread” here is a separate operating-system process, not an in-memory thread. Hold that, and the rest follows.
Not the thread you might expect
In many languages a thread shares memory with the code that started it. Winter Thread does
not do that. Each Thread you start is a separate PHP process, spawned by the OS,
with its own memory, its own file handles, its own everything.
Think of it less like a second worker at your desk sharing your notes, and more like mailing a sealed envelope to a colleague in another building: they open it, do the work, and mail you back a result. They never touch your desk.
This is a deliberate trade-off:
- Isolation — a fatal error, memory leak, or segfault in a child cannot corrupt or crash your main script or its siblings.
- True parallelism — the OS can schedule each process on a different CPU core.
- In exchange — no shared memory, so you can’t just read a variable the child changed.
The serialization boundary
Here’s the part worth internalizing. When you start a task, the Runnable object is
serialized (frozen into a string), shipped to the new process, and deserialized
(thawed) on the other side. Only then does run() execute.
Parent process Child process
-------------- -------------
new ReportGenerator(42)
│ serialize
▼
"...frozen bytes..." ──────► deserialize → ReportGenerator(42)
│
▼
run()Two consequences fall out of this, and they explain rules you’ll meet everywhere:
- No live resources in properties. A database connection, open file, or socket can’t
be frozen and shipped. Keep the constructor to plain data (ids, paths, flags) and open
connections inside
run(). - Changes in the child stay in the child. Anything
run()mutates on$this— or in memory — is gone when the process exits. To get results back, write to a file or database, or signal success through the exit code.
This is why
Every “create resources inside run()” and “properties must be serializable” note in these docs traces back to this one boundary. It’s not a quirk — it’s the price of isolation.
What happens when you call start()
Gently, end to end:
- Your parent script serializes the
Runnable. - It launches a new PHP process running an internal runner script.
start()returns the child’s PID immediately — your script is never blocked.- The runner deserializes the task and calls
run(). - When you call
join(), the parent waits for that process to finish and reads its exit code.
That’s the whole life cycle. The gritty version — descriptors, the runner internals, how output is wired — lives in Deep dive → Runner life cycle.
When to reach for it
- ✅ Offloading slow jobs (encoding, reports, batch emails) so a request stays responsive.
- ✅ Running several independent jobs at once across cores.
- ❌ Tasks that must constantly share and mutate the same in-memory state — processes don’t share memory; coordinate through files, a database, or a queue instead.
Next
- Do it hands-on: Quickstart
- Task recipes: Queue worker · Parallel tasks
- The authoritative surface: API reference