Parallel Processing with PHP (Part 3): multiplexed Inter-Process Communication with `stream_select()`

In PHP, process forking via pcntl_fork() offers a powerful way to execute tasks in parallel, a technique particularly useful for building CLI daemons, task runners, and background processors. In Part 1: Parallel Processing with PHP: Why, How, and When, we explored the motivations and mechanics of spawning child processes using pcntl_fork(). In Part 2: Inter-Process Communication, we took it a step further by introducing basic IPC (Inter-Process Communication) using stream_socket_pair(). This article is a natural continuation of that series. Here, we’ll focus on the parent real-time orchestration of multiple child processes using stream_select(), a powerful tool for monitoring multiple communication channels concurrently. More specifically, we'll explore how the parent process can: Fork and create multiple child processes. Implement multiplexed IPC, allowing scalable, event-driven process management. Efficiently read messages from multiple children. By the end, you’ll understand how to use core PHP tools to transform a simple forking setup into a multi-stream communication model. Why fork multiple processes? Forking allows a parent process to create child processes, each capable of performing a separate task simultaneously. This is crucial when: Tasks are independent and can run in parallel (e.g., batch processing, downloads). You want to maximize CPU usage on multicore systems. You need a controller (the parent) to orchestrate, monitor, and communicate with children. However, once multiple child processes are running, the parent has new responsibilities: Track which child is running. Detect when a child terminates. Receive messages or results from each child. Parent-orchestrated model The parent process isn't just a bystander. It must act as a coordinator: It forks each child using pcntl_fork(). It creates bidirectional sockets using stream_socket_pair() for IPC. It uses stream_select() to efficiently listen to all children simultaneously. This design enables a powerful architecture in which the parent acts as a "supervisor", reacting immediately when any child sends data without being stuck waiting on just one. Creating communication channels with stream_socket_pair() PHP's stream_socket_pair() creates a pair of connected sockets — think of them as two ends of a telephone line. One end is handed to the parent, the other to the child. Example: $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); $pair[0]: Used by the parent. $pair[1]: Used by the child. These sockets allow full-duplex communication: you can write and read from both ends. Monitoring many sockets with stream_select() Once multiple child processes are forked and communication sockets are set up, the parent must wait for messages, and react immediately as soon as one of more messages come from children. What is stream_select()? stream_select() is a function that watches multiple streams (e.g., sockets, files) and tells you which ones are ready for reading, writing, or have errors. It’s non-blocking and works efficiently for multiple children. Syntax: stream_select(array &$read, array &$write, array &$except, int $timeout_seconds); $read: Streams to check for incoming data. $write: Streams ready to send data (optional). $except: Streams with errors (optional). Timeout: How long to wait in seconds (or 0 for immediate return, or null for infinite). Why it matters: Without stream_select(), the parent would have to block on fread() or poll each child manually. With stream_select(), the parent can instantly react to the first child that sends data. With stream_select(), the parent can instantly react even if multiple childrend send data. Practical example Below is a practical example demonstrating how to fork multiple child processes in PHP and establish real-time communication with each one using stream_socket_pair() and stream_select(). This pattern allows the parent process to efficiently listen for messages from any number of children concurrently, without polling each socket individually.

May 12, 2025 - 07:48
 0
Parallel Processing with PHP (Part 3): multiplexed Inter-Process Communication with `stream_select()`

In PHP, process forking via pcntl_fork() offers a powerful way to execute tasks in parallel, a technique particularly useful for building CLI daemons, task runners, and background processors.
In Part 1: Parallel Processing with PHP: Why, How, and When, we explored the motivations and mechanics of spawning child processes using pcntl_fork().
In Part 2: Inter-Process Communication, we took it a step further by introducing basic IPC (Inter-Process Communication) using stream_socket_pair().

This article is a natural continuation of that series. Here, we’ll focus on the parent real-time orchestration of multiple child processes using stream_select(), a powerful tool for monitoring multiple communication channels concurrently.

More specifically, we'll explore how the parent process can:

  • Fork and create multiple child processes.
  • Implement multiplexed IPC, allowing scalable, event-driven process management.
  • Efficiently read messages from multiple children.

By the end, you’ll understand how to use core PHP tools to transform a simple forking setup into a multi-stream communication model.

Why fork multiple processes?

Forking allows a parent process to create child processes, each capable of performing a separate task simultaneously. This is crucial when:

  • Tasks are independent and can run in parallel (e.g., batch processing, downloads).
  • You want to maximize CPU usage on multicore systems.
  • You need a controller (the parent) to orchestrate, monitor, and communicate with children.

However, once multiple child processes are running, the parent has new responsibilities:

  1. Track which child is running.
  2. Detect when a child terminates.
  3. Receive messages or results from each child.

Parent-orchestrated model

The parent process isn't just a bystander. It must act as a coordinator:

  • It forks each child using pcntl_fork().
  • It creates bidirectional sockets using stream_socket_pair() for IPC.
  • It uses stream_select() to efficiently listen to all children simultaneously.

This design enables a powerful architecture in which the parent acts as a "supervisor", reacting immediately when any child sends data without being stuck waiting on just one.

Creating communication channels with stream_socket_pair()

PHP's stream_socket_pair() creates a pair of connected sockets — think of them as two ends of a telephone line. One end is handed to the parent, the other to the child.

Example:

$pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
  • $pair[0]: Used by the parent.
  • $pair[1]: Used by the child.

These sockets allow full-duplex communication: you can write and read from both ends.

Monitoring many sockets with stream_select()

Once multiple child processes are forked and communication sockets are set up, the parent must wait for messages, and react immediately as soon as one of more messages come from children.

What is stream_select()?

stream_select() is a function that watches multiple streams (e.g., sockets, files) and tells you which ones are ready for reading, writing, or have errors. It’s non-blocking and works efficiently for multiple children.

Syntax:

stream_select(array &$read, array &$write, array &$except, int $timeout_seconds);
  • $read: Streams to check for incoming data.
  • $write: Streams ready to send data (optional).
  • $except: Streams with errors (optional).
  • Timeout: How long to wait in seconds (or 0 for immediate return, or null for infinite).

Why it matters:

  • Without stream_select(), the parent would have to block on fread() or poll each child manually.
  • With stream_select(), the parent can instantly react to the first child that sends data.
  • With stream_select(), the parent can instantly react even if multiple childrend send data.

Practical example

Below is a practical example demonstrating how to fork multiple child processes in PHP and establish real-time communication with each one using stream_socket_pair() and stream_select().
This pattern allows the parent process to efficiently listen for messages from any number of children concurrently, without polling each socket individually.




$sockets = [];

// Define a list of anonymous functions that simulate work by sleeping
$functions = [
    fn () => sleep(2),
    fn () => sleep(3),
    fn () => sleep(5),
    fn () => sleep(2),
    fn () => sleep(2),
];

foreach ($functions as $index => $fn) {
    // Create a socket pair for parent-child communication
    $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);

    // Fork the process
    $pid = pcntl_fork();

    if ($pid === 0) {
        // --- CHILD PROCESS BLOCK ---

        fclose($pair[0]); // Close parent end in the child

        // Send initial message
        fwrite($pair[1], "Child $index START");

        // Execute the task (simulated by sleep)
        $fn();

        // Send a mid-execution message
        fwrite($pair[1], "Child $index DOING");

        // Execute the task again
        $fn();

        // Send final message before exiting
        fwrite($pair[1], "Child $index ENDED");

        fclose($pair[1]); // Close socket to signal end-of-stream
        exit(0); // Important to prevent child from continuing in the loop
    } else {
        // --- PARENT PROCESS BLOCK ---

        fclose($pair[1]); // Close child end in the parent

        // Set the parent-side socket to non-blocking mode
        stream_set_blocking($pair[0], false);

        // Store the socket indexed by child PID
        $sockets[$pid] = $pair[0];
    }
}

// --- PARENT EVENT LOOP ---

while (! empty($sockets)) {
    // Prepare read set for stream_select()
    $read = array_values($sockets);
    $write = $except = null;
    $timeout = 2; // Timeout in seconds for stream_select()

    // Wait for one or more child sockets to become readable
    $changed = stream_select($read, $write, $except, $timeout);

    if ($changed > 0) {
        echo '[PARENT]';
        foreach ($read as $sock) {
            // Find the PID associated with this socket
            $pid = array_search($sock, $sockets, true);

            // Read data from the child (up to 1024 bytes)
            $data = fread($sock, 1024);

            if ($data === false || $data === '') {
                // Socket closed or end-of-stream reached
                fclose($sock);
                unset($sockets[$pid]); // Remove from active set
            } else {
                // Display the message received from the child
                echo " Message from child $pid: $data ";
            }
        }
        echo "[/PARENT]\n";
    } else {
        // Timeout occurred — no child sockets had data
        echo "timeout ...\n";
    }
}

Understanding the output

The diagram of a process that forks muliple process and handles the communication

When you run the script, you’ll see output like the following:

[PARENT] Message from child 62909: Child 0 START  Message from child 62910: Child 1 START  Message from child 62911: Child 2 START [/PARENT]
[PARENT] Message from child 62912: Child 3 START [/PARENT]
[PARENT] Message from child 62913: Child 4 START [/PARENT]
timeout ...
[PARENT] Message from child 62909: Child 0 DOING  Message from child 62912: Child 3 DOING [/PARENT]
[PARENT] Message from child 62913: Child 4 DOING [/PARENT]
[PARENT] Message from child 62910: Child 1 DOING [/PARENT]
[PARENT] Message from child 62909: Child 0 ENDED  Message from child 62912: Child 3 ENDED  Message from child 62913: Child 4 ENDED [/PARENT]
[PARENT][/PARENT]
[PARENT] Message from child 62911: Child 2 DOING [/PARENT]
[PARENT] Message from child 62910: Child 1 ENDED [/PARENT]
[PARENT][/PARENT]
timeout ...
[PARENT] Message from child 62911: Child 2 ENDED [/PARENT]
[PARENT][/PARENT]

What’s happening here?

  • [PARENT] ... [/PARENT] These brackets wrap all the messages received from children during a single stream_select() cycle. They show that the parent is actively handling data from one or more children during that particular iteration.
  • Multiple messages inside one [PARENT] ... [/PARENT] block This shows that stream_select() detected multiple sockets as ready to read, and the parent collected messages from more than one child process in the same pass. This illustrates the multiplexed nature of stream_select(), you don’t need one thread or blocking call per child; you can react to all readable sockets at once.
  • timeout ... This line is printed when no child sends any messages within the timeout period (2 seconds, in this example). This gives the parent an opportunity to perform housekeeping, print status updates, or continue looping without freezing.
  • [PARENT][/PARENT] with no messages This occurs when a child closes its socket. When a socket reaches EOF, stream_select() still returns it as readable, but fread() returns an empty string. That’s the parent’s signal to close and clean up that socket.

When is stream_select() a good choice?

Moderate concurrency (some, not thousands)

If you're handling a limited number of sockets, stream_select() is perfectly fine. It avoids threading and keeps your code readable and event-driven.

One-off or lightweight parallelism in CLI PHP

In command-line PHP scripts where you're forking a few child processes and want simple inter-process communication, stream_select() is ideal, as shown in this article.

Listening multiple streams

If you want to poll for input on multiple streams or sockets for example:

  • Reading from multiple pipes or sockets
  • Managing inter-process or network messages
  • Waiting on both STDIN and a network socket

...then stream_select() gives you a clean, non-threaded way to multiplex.

Educational / Proof of Concept work

For learning and demonstrating how event-driven programming works, stream_select() is easy to understand and trace. It’s a great teaching tool before introducing more advanced models.

Typical use cases in PHP:

  • Parallel execution of background tasks via pcntl_fork() + socket pairs
  • Building small daemon-like scripts for system monitoring
  • Lightweight socket servers or testing tools
  • Coordinating multiple child processes without shared memory

When stream_select() falls short: CPU-bound parent processing

While stream_select() is great for monitoring multiple input streams, it runs in a single process, the parent. This introduces a performance bottleneck in scenarios where:

  • Many child processes are sending frequent messages.
  • The parent is responsible for reading and then processing each message.
  • The processing logic is non-trivial, for example, parsing, database access, complex computation, etc.

In such cases, even if stream_select() efficiently notifies the parent that data is available, the actual reading and processing still happen sequentially. This means that slow handling of one message can delay processing others, leading to a backlog or dropped responsiveness.

Example Scenario:

In the demo code, the parent simply prints received messages, a negligible operation. But imagine instead that the parent:

  • Analyzes the data
  • Sends a response
  • Logs to disk

Now, the parent becomes a bottleneck, because stream_select() doesn’t distribute the workload — it just detects it. For high-load systems, you'd want to offload processing to worker threads, use a job queue, or architect a system with parallel message handling, possibly via multiple parent-like handlers or a dedicated event loop with worker pools.

So, when stream_select() is not recommended

  • Scalability Limits (FD_SETSIZE): stream_select() has a hard limit on the number of file descriptors it can monitor, typically 1024 on many systems. This makes it unsuitable for high-concurrency applications like real-time chat servers or large-scale socket listeners.
  • Inefficient with many file descriptors: stream_select() must linearly scan the file descriptor sets for activity. As the number of descriptors grows, performance degrades significantly.
  • Lack of event granularity: stream_select() can tell you a file descriptor is ready, but not what changed. This means your application still has to read/write or probe the socket to determine what to do next.
  • Platform-specific behavior: some edge behaviors (like how closed sockets are reported or timeout precision) can differ between platforms, leading to inconsistencies, especially in portable code.
  • Can miss readiness due to buffering: In PHP, streams are buffered. So even if the underlying OS marks a socket as "readable," fread() might return nothing until the buffer is filled, leading to false positives or confusing behavior.
  • Not Ideal for High-Frequency Low-Latency Tasks: If you need microsecond precision or very fast event handling (e.g., trading systems, games, real-time analytics), stream_select() is too coarse and slow.
  • No built-in event loop management: unlike libevent, ReactPHP, or Amp, stream_select() provides no abstraction for timers, idle callbacks, or signal, you have to manage all that manually.

Use cases to avoid stream_select() in PHP

  • Large-scale websocket servers (use ReactPHP, Swoole, or RoadRunner)
  • Applications handling hundreds or thousands of concurrent sockets
  • Systems needing sub-millisecond responsiveness or fine-grained event control
  • When targeting platforms where PHP stream wrappers behave inconsistently

Conclusion

By combining pcntl_fork(), stream_socket_pair(), and stream_select(), PHP developers can build simple concurrent systems with real-time process coordination.

The parent-child model with monitored IPC streams allows for the writing of organized PHP background workers and daemons.