Skip to content

About

Learn Swoole by Examples

Topics

Resources

Stars

216 stars

Watchers

13 watching

Forks

Repository files navigation

Swoole by Examples

Tests License: CC BY-NC-ND 4.0

Swoole is a PHP extension that turns PHP into a long-running, asynchronous runtime, much like Node.js or Go. With PHP-FPM, every request starts from scratch, blocks on every database query or HTTP call, and throws everything away when it ends. A Swoole application starts once, keeps objects and connections in memory between requests, and runs thousands of concurrent tasks in a single process.

This repository teaches Swoole through small, runnable examples, one concept per script. Every example runs in the provided Docker containers, and nearly every one is covered by an automated test that runs in CI.

Table of contents

Supported versions

All the examples are written for PHP 8.4+ and Swoole 6.2+, and tested on PHP 8.4 with Swoole 6.2. The Docker images, the Composer requirements, and the CI workflows all use these versions; the examples may not work on older versions of PHP or Swoole. You don't need PHP, Swoole, or Composer installed locally: the only requirement is Docker with Compose v2 (the docker compose command).

Quick start

git clone https://github.com/deminy/swoole-by-examples.git
cd swoole-by-examples
docker compose up -d   # give it a few seconds for the servers to start

# Run a first example: 2,000 coroutines that each sleep for 1 second, all finishing in about 1 second.
docker compose exec -t client bash -c "time ./csp/coroutines/many-coroutines-in-a-loop.php"

How to run the examples

docker compose up -d starts three PHP containers, plus the redis, mysql, and postgresql services that the database and Redis examples connect to. The PHP containers mount this repository at /var/www, with /var/www/examples as the working directory (which is why run commands use paths like ./csp/channel.php):

Container What runs there How to use it
client Standalone scripts, and clients that talk to the servers docker compose exec -t client bash -c "./csp/channel.php"
server 8 long-running example servers (HTTP, WebSocket, TCP, UDP, ...), started automatically by Supervisord and restarted whenever a PHP file under examples/ changes Nothing to start: connect to server:<port> from the client container
zts The thread examples, which need a thread-safe (ZTS) build of PHP docker compose exec -t zts php ./threads/map.php
  • Every example documents its exact run command in its docblock, along with any extra steps. The "Run from" column in the tables below says where to run it: client, server, zts, or "auto-started" for the servers that are already running.
  • Run examples from the client container unless the docblock says otherwise. The two images differ: for example, only client has the mysqli extension, and only server has APCu and System V message queues.
  • Server ports are not published to your host (except port 9801, for Rock Paper Scissors), so send requests from the client container, e.g. docker compose exec -t client curl -i http://server:9501.
  • To follow the servers' output, run docker compose logs -f server. To open a shell, run docker compose exec -ti client bash (or server). To stop everything, run docker compose down.

The images are built from the official Swoole image (phpswoole/swoole:6.2-php8.4) with a few extra extensions and tools; see dockerfiles/. The repository swoole/docker-swoole has more examples of using that image.

Key concepts in 60 seconds

  • Coroutine: a lightweight function that can pause and resume. When a coroutine waits on I/O (a query, an HTTP call, a sleep), Swoole runs another coroutine instead of blocking the whole process. Thousands of them can run in a single process.
  • Event loop (scheduler): the part of Swoole that decides which coroutine runs next, and wakes coroutines up when their I/O is ready.
  • CSP and channels: the concurrency style Swoole shares with Go (CSP stands for Communicating Sequential Processes). Coroutines pass data to each other through channels instead of sharing variables.
  • Runtime hooks: the switch that makes ordinary blocking PHP code (sleep(), PDO, mysqli, curl, phpredis, file functions, ...) non-blocking inside coroutines, without changing the code. Swoole\Coroutine\run() turns them on for a script; a server turns them on with its hook_flags setting.
  • Servers: Swoole has built-in HTTP, WebSocket, TCP, UDP, and other servers, with no Nginx or PHP-FPM in front. A server runs several worker processes (managed by a manager process), and your code reacts to events such as "a request arrived".
  • Processes and shared memory: for CPU-bound work or isolation, Swoole manages pools of worker processes, which share data through shared-memory structures (Swoole\Table, Swoole\Atomic) and locks.

Start here: why Swoole?

The same work, done the PHP-FPM way and the Swoole way. Read these in order.

Example What it shows Run from
Blocking I/O A plain PHP script whose two simulated I/O calls run one after the other, taking about 3 seconds client
Non-blocking I/O (debug version) The same work in two coroutines that wait at the same time, taking about 2 seconds; the debug version prints the order in which they interleave client
Blocking vs non-blocking A function that starts a coroutine returns as soon as that coroutine pauses in a non-blocking way, while the coroutine keeps running in the background client
1,000,000 coroutines How cheap coroutines are: start a million of them in one process, each sleeping for 5 seconds (needs about 8 GB of RAM) client

Coroutine basics

Example What it shows Run from
Ways to create a coroutine go(), Swoole\Coroutine::create(), Co::create(), and the other equivalent ways to start a coroutine client
Callback types Any PHP callable (closure, function name, static or instance method) can be the body of a coroutine client
Many coroutines in a loop 2,000 one-second sleeps finish in about 1 second instead of about 2,000 client
Nested coroutines (execution order) Coroutines started inside other coroutines, and the order in which they run client
Yield and resume Pause a coroutine by hand, and resume it later from another coroutine client
Cancel a coroutine Swoole\Coroutine::cancel() interrupts a coroutine's sleep or channel wait from another coroutine, so it can stop early client
Block a coroutine Waiting (e.g. sleep() or Channel::pop()) pauses only the current coroutine; the rest of the process keeps running client
defer Register cleanup callbacks that run, in reverse order, when a coroutine finishes client
Context Per-coroutine storage that is cleaned up automatically when the coroutine ends; the coroutine-safe replacement for globals client
Exit from a coroutine exit() inside a coroutine throws Swoole\ExitException instead of ending the process; prefer throwing your own exception client

Coordinating coroutines

Example What it shows Run from
Channel The main way coroutines pass data to each other; push() and pop() wait automatically when the channel is full or empty client
WaitGroup Wait for a group of coroutines to finish, like Go's sync.WaitGroup client
Barrier The same goal as WaitGroup with less code: the wait ends once every coroutine has released its reference to the barrier client
Lock across coroutines Swoole\Coroutine\Lock: a mutex that suspends only the waiting coroutine, not the whole process client

Runtime hooks

Runtime hooks make existing blocking PHP functions and extensions (curl, PDO, mysqli, Redis, sleep(), ...) run concurrently inside coroutines, without changing their code. In the database and Redis examples below, five 3-second operations finish in about 3 seconds instead of 15.

Example What it shows Run from
Enable and disable runtime hooks Turn coroutine support on and off in a standalone script (Swoole\Coroutine\run() turns all hooks on by default) client
Hook flags Choose which kinds of blocking functions get hooked: coroutines with the hooks enabled run concurrently, the others block client

curl

Both examples send their requests to the auto-started HTTP/1 server in the server container.

Example What it shows Run from
SWOOLE_HOOK_NATIVE_CURL (recommended) Hooks the real curl extension, including curl_multi_*(): six 2-second requests finish in about 2 seconds client
SWOOLE_HOOK_CURL The older approach, implemented in Swoole Library; doesn't support curl_multi_*() client

Databases

Example What it shows Run from
mysqli Five concurrent MySQL queries using the mysqli extension client
PDO_MYSQL Five concurrent MySQL queries using PDO client
PDO_PGSQL Five concurrent PostgreSQL queries using PDO client
PDO_SQLITE Five concurrent SQLite writes using PDO (the delay comes from waiting on SQLite's own database lock) client

Redis

Example What it shows Run from
phpredis Five concurrent slow Redis operations using the phpredis extension client
predis The same using the pure-PHP predis library client, after composer global require predis/predis=~3.0

Connection pools

Once database calls run concurrently, a pool shares a bounded set of connections among the coroutines instead of opening one connection per coroutine.

Example What it shows Run from
MySQL connection pool 1,024 one-second queries through 128 pooled connections finish in about 8 seconds client
PostgreSQL connection pool The same pattern with PDO_PGSQL client
Redis connection pool The same pattern with phpredis client

For a custom pool, see crowdstar/vertica-swoole-adapter, which implements a connection pool for HP Vertica databases through ODBC.

Coroutine clients

Swoole's own non-blocking clients, for use inside coroutines. Most of them talk to the servers running in the server container; the low-level socket example creates its own server socket.

Example What it shows Run from
HTTP/1 client Two requests made concurrently; the faster one finishes first client
HTTP/2 client Multiplex concurrent requests as streams over a single TCP connection client
WebSocket client Connect, send a message, and read the reply client
TCP client Query two TCP servers concurrently client
UDP client Send a datagram and read the reply client
Low-level socket Swoole\Coroutine\Socket: a server socket and two client sockets exchanging length-prefixed messages client

Servers

Some servers below are auto-started in the server container, and their docblocks show how to send requests to them. The others ("client" in the Run from column) start their own server on a random port, send it requests, print what happened, and shut down, so they run like any other script.

A server runs in one of two modes: in SWOOLE_BASE mode (the default), each worker process accepts and handles its own connections; in SWOOLE_PROCESS mode, a master process owns all the connections and forwards their data to the worker processes, so any worker can reach any connection (see WebSocket broadcasting).

Your first server

Example What it shows Run from
HTTP/1 server Custom status codes, static file serving, and gzip compression auto-started, port 9501
Server events Which callbacks (onStart, onWorkerStart, onReceive, onTask, ...) fire, in which process, and in what order, including during a reload client
Coroutines in a server The enable_coroutine, task_enable_coroutine, and hook_flags settings, and why a server enables no runtime hooks by default client
Coroutine-style HTTP server Swoole\Coroutine\Http\Server: a server that runs inside a coroutine in the current process, with no worker processes client
Hot reload Load updated code into a running server with $server->reload(), which restarts the worker processes gracefully client

One server per protocol

Example What it shows Run from
HTTP/2 server A minimal HTTP/2 server auto-started, port 9503
HTTPS server Serve HTTP over SSL/TLS, and see a client reject an untrusted certificate and accept a trusted one client
Server-Sent Events (SSE) Stream a response chunk by chunk over HTTP/1.1, the technique many AI chat apps use to stream text client
WebSocket server A minimal WebSocket server that replies to messages auto-started, port 9504
WebSocket broadcasting Send a message to every connected client, across worker processes, as in a chat room client
TCP server, event-driven style Register callbacks such as onReceive, and let the server call them auto-started, port 9505
TCP server, coroutine style Write the accept/read/write loop yourself inside coroutines, like a Go server auto-started, port 9507
UDP server An echo server for UDP datagrams auto-started, port 9506
UDP multicast A UDP server that joins an IP multicast group and receives datagrams sent to the group address client
Redis server A server speaking the Redis protocol (minimal GET/SET), usable from any Redis client client
MQTT broker A minimal publish/subscribe broker built on the open_mqtt_protocol setting, tested with the Mosquitto command-line clients client
Reverse proxy A TCP-level reverse proxy relaying each connection to an upstream HTTP server client

Multiple ports and protocols

Example What it shows Run from
Listening on multiple ports One server listening on two ports, each with its own callbacks client
Different protocols on different ports One server speaking HTTP on one port and raw TCP on another client
Several protocols on the same port HTTP/1, HTTP/2, and WebSocket served on one port client

Connection health and protection

Example What it shows Run from
Heartbeat The server closes connections that have sent nothing for a given number of seconds client
TCP keepalive Let the operating system probe idle connections and drop dead ones client
Delayed receive (DDoS protection) Delay reading from a new connection until your code approves it (enable_delay_receive and Server::confirm()) client

State inside a server

Example What it shows Run from
APCu caching APCu works in Swoole the same way as in any PHP CLI application; per-worker request counters show that all worker processes share the cache server

Processes and shared memory

Process pools

Swoole\Process\Pool keeps a fixed set of worker processes running, restarting any that exit. The examples differ in how work reaches the workers through IPC (inter-process communication). In the message-queue, TCP and Unix-socket examples, one worker process plays the client and sends work to the others.

Example What it shows Run from
Standalone pool Workers just run your code; nothing is sent to them client
Pool with a message queue Send work to the pool through a System V message queue server
Pool with a TCP socket Send work to the pool over TCP client
Pool with a Unix socket Send work to the pool over a Unix socket client
Detach a worker Let a worker escape the pool manager's control to finish a long task at its own pace client

Sharing data between processes

Example What it shows Run from
Swoole\Table A fixed-schema, in-memory table shared by all processes, with atomic counter updates client
Atomic counter, unsigned 32-bit Swoole\Atomic: a shared-memory counter that is safe to update from many processes client
Atomic counter, signed 64-bit Swoole\Atomic\Long: the same for signed 64-bit integers client

Synchronizing processes

Example What it shows Run from
Lock across processes Swoole\Lock: a shared-memory mutex that blocks the whole waiting process (never use it across coroutines; see Deadlocks) client
Block one process with a lock Block a process on a Swoole\Lock with a timeout client
Block processes with a lock Block several processes on a shared Swoole\Lock, then release them one by one from another process client
Block and wake up a process with an atomic One process blocks on a shared Swoole\Atomic with wait() (with and without a timeout) until another process wakes it up with wakeup() client

Threads

Swoole 6 can also run PHP code in multiple threads (Swoole\Thread). Threads don't share PHP variables; they share data through thread-safe containers instead. These examples need a thread-safe (ZTS) build of PHP, so they run in the zts container.

Example What it shows Run from
Lock across threads Swoole\Thread\Lock: a mutex shared by threads zts
Shared map Swoole\Thread\Map: four threads update one map; an atomic incr() never loses an update, a read-then-write does zts
Work queue Swoole\Thread\Queue: worker threads take jobs from a shared queue, the basic shape of a thread pool zts

Signals and the event loop

Example What it shows Run from
Default exit condition Signal listeners alone don't keep a process running: it exits before any signal arrives client
Custom exit condition The exit_condition option keeps the event loop alive while signal listeners are registered client
Wait for a signal in a coroutine Swoole\Coroutine\System::waitSignal() suspends the calling coroutine until a signal arrives or a timeout expires, with no callbacks client

Timers and cron jobs

Example What it shows Run from
Swoole\Timer Run code after a delay or at a fixed interval, like JavaScript's setTimeout() and setInterval() client
Timers with coroutines The same, implemented with plain coroutines and sleeps client

The examples below run recurring jobs (cron jobs) in seven different ways, without the system cron.

Standalone cron jobs

The scheduler is a program of its own, deployed and supervised independently.

Example What it shows Run from
Using timers Fixed-rate scheduling with Swoole\Timer client
Using a coroutine loop Overlap-free scheduling with a plain coroutine sleep client
Using an interruptible sleep A channel-based sleep that a shutdown can interrupt instantly client
Using a process pool The schedule sharded across the workers of a process pool client

Cron jobs inside a server

The scheduler runs inside an application server, following the server's lifecycle and sharing its state.

Example What it shows Run from
Timer plus task workers A timer in one server worker hands scheduled work to task worker processes, so slow jobs don't stall requests client
Dedicated user process An isolated scheduler process attached to a server (Server::addProcess()), with graceful shutdown on SIGTERM client
Interruptible sleep in a server Let a cron job inside a web server run one last time when the server shuts down, instead of being cut off mid-interval client

Putting it all together

Example What it shows Run from
Integrated HTTP/1 server One HTTP server that also runs cron jobs, and hands slow work to task worker processes (task(), taskwait(), taskWaitMulti(), taskCo()) auto-started, port 9502
Integrated WebSocket server A WebSocket server with separate processes for a cron job and for consuming a task queue client
Rock Paper Scissors The server holds the first two players' HTTP requests open, and answers all three players at once when the last one arrives, which PHP-FPM can't do auto-started, port 9801 (also open on your host: http://127.0.0.1:9801?name=A)

Pitfalls and advanced topics

Deadlocks: how they happen

A deadlock happens when every coroutine is waiting and nothing can wake them up. When coroutines wait on Swoole's own primitives (such as channels), Swoole detects this and reports it. When they block on something Swoole can't see, such as a file lock or a Swoole\Lock, the program just hangs.

Example What it shows Run from
Pop from an empty channel The only coroutine waits for data that never comes client
Push to a full channel The only coroutine waits for space that never frees up client
File locking Lock a file that is already locked and never released: the program hangs, with no deadlock report client
Swoole\Lock across coroutines Acquire a locked Swoole\Lock from another coroutine: the program just hangs client
Server shutdown Shut down a server while a coroutine is still running client

Deadlocks: detecting and handling them

Each of these examples creates a deadlock on purpose, by pausing the only coroutine in the program forever.

Example What it shows Run from
Default behavior Swoole reports the deadlock and the coroutines involved client
Deadlock check disabled Turn off the deadlock report with enable_deadlock_check client
Custom exit condition Replace the deadlock check with your own rule for when the program may exit (this demo waits forever) client

CPU-bound code and scheduling

Coroutines normally switch only when one of them waits on I/O, so a CPU-heavy loop can starve the others. Read these in order.

Example What it shows Run from
Non-preemptive scheduling The default: a busy coroutine never gives up the CPU, so the other coroutine never runs client
Preemptive scheduling With swoole.enable_preemptive_scheduler on, Swoole interrupts the busy coroutine every few milliseconds so others get a turn client
Toggle the preemptive scheduler Turn preemption off around a critical section, and back on afterwards client

Testing Swoole code

With plain PHPUnit, tests run one at a time and outside of any coroutine, so tests that wait on I/O add up, and test code can't call coroutine APIs directly. deminy/counit runs each test method in its own coroutine instead, so tests that wait on I/O run concurrently.

This repository's own test suite uses it to run a test for nearly every example:

  • phpunit.xml.dist registers the Deminy\Counit\CounitExtension extension.
  • The base test class tests/Support/ExampleTestCase.php extends Deminy\Counit\TestCase.
  • There is one test class per topic under tests/. Most tests run their example as a subprocess and check its output, with at most 8 examples running at once; the auto-started servers are tested by connecting to them, either directly with Swoole's coroutine clients or through the client examples.

To run the tests (composer.json is at the repository root, hence the -w /var/www):

docker compose exec -T -w /var/www client composer install -n -q --no-progress
docker compose exec -T -w /var/www client ./vendor/bin/counit --testsuite client
docker compose exec -T -w /var/www server ./vendor/bin/counit --testsuite server
docker compose exec -T -w /var/www zts ./vendor/bin/counit --testsuite zts

Almost every test is in the client suite. The server suite holds the tests for the examples that need PHP extensions installed only in the server container (APCu and "sysvmsg"), and the zts suite holds the tests for the thread examples. The client suite takes about a minute.

Why some tests run in a separate process, and which example isn't tested
  • Tests for examples that may hang forever by design (the deadlock demos), that block the whole process (the io/block-*-using-swoole-lock.php examples), or that depend on the preemptive scheduler run one at a time in a separate, non-coroutine process, using PHPUnit's #[RunInSeparateProcess] attribute and ExampleTestCase::runIsolated(). Running many such subprocesses concurrently proved unreliable, so these trade speed for reliability, and they account for most of the suite's running time.
  • csp/coroutines/benchmark.php is skipped (it creates 1,000,000 coroutines).

For examples of unit-testing coroutine code in the same process, see the counit documentation.

License

The examples are licensed under CC BY-NC-ND 4.0; see LICENSE.txt.

About

Learn Swoole by Examples

Topics

Resources

Stars

216 stars

Watchers

13 watching

Forks

Used by

Contributors

Languages