Package · thread

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.

text
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:

  1. 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().
  2. 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:

  1. Your parent script serializes the Runnable.
  2. It launches a new PHP process running an internal runner script.
  3. start() returns the child’s PID immediately — your script is never blocked.
  4. The runner deserializes the task and calls run().
  5. 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

Great for independent, long-running, or blocking workNot for shared-memory parallelism or fine-grained IPC
  • ✅ 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