With the release of PHP 8.1, developers using ZTS (Zend Thread Safety) and the parallel extension gained another convenient tool for inter-thread communication: a shared in-memory SQLite database that can be configured with a single DSN string. This approach turns out to be simpler and more intuitive than the standard parallel\Channel or custom sockets.
PHP with ZTS enabled allows code to run concurrently in multiple threads. The parallel extension provides a high-level API for this: parallel\Runtime, parallel\Future, parallel\Channel. Threads are isolated, so to exchange data you must either use channels (parallel\Channel) or implement your own data exchange mechanism via sockets, files, or shared memory. While this works, it requires extra code/extensions/experience and is not always convenient.
Starting with PHP 8.1, PDO SQLite supports a special DSN format using URIs, which allows the same in-memory database to be opened from different threads. The key connection string is:
$pdo = new \PDO('sqlite:file:my_channel?mode=memory&cache=shared');file:my_channel— the channel name (can be anything);mode=memory— the database exists only in RAM;cache=shared— a shared cache, allowing other connections with the same channel name to work with the same database.
If you create such a connection in the main thread and then open a PDO with exactly the same DSN in each child thread, all of them will see the same tables, sharing memory among themselves. This turns SQLite into an ideal “data bus” for inter-thread communication.
The main thread creates a task table and starts several workers:
// main thread
$channel = new \PDO('sqlite:file:tasks?mode=memory&cache=shared');
$channel->exec('CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
payload TEXT,
status TEXT
)');
$callback = static function() {
$channel = new \PDO('sqlite:file:tasks?mode=memory&cache=shared');
// Modify and retrieve a task; this query protects writes from other threads
$sql = 'UPDATE tasks SET status = "progress" WHERE id IN (
SELECT id FROM tasks WHERE status = "pending" LIMIT 1
) RETURNING *';
while (true) {
// Fetch a task, process it, update its status
$stmt = $channel->prepare($sql);
$stmt->execute();
$task = $stmt->fetch(PDO::FETCH_ASSOC);
if ($task) {
// Process the task
} else {
\usleep(10_000); // 10ms pause to avoid high CPU usage
}
}
};
// Launch threads
$runtimes = [];
for ($i = 0; $i < 4; $i++) {
$runtime = new \parallel\Runtime();
$runtime->run($callback);
$runtimes[] = $runtime;
}
// Wait for completion
foreach ($runtimes as $runtime) {
$runtime->close();
}Now you can add tasks in the main thread using the same PDO connection, and workers will pick them up. The exchange can be made bidirectional; the possibilities are limited only by your imagination.
- You use plain SQL, which is familiar to any PHP developer.
- The
parallel\Channel, Events, and Sync APIs require explicit lock management and message queues, which can easily lead to deadlocks if resource acquisition order is violated or during asynchronous send/recv operations. In contrast, declarative work with SQLite handles concurrency at the database level. - You can store any data structures (via JSON or serialization), perform complex queries, groupings, and use indexes.
- No need for additional extensions like Redis, Memcached, etc.; SQLite is usually already available in PHP.
- Keep a reference to the PDO object; if it is destroyed before threads connect to the database, you will lose the data.
- If you need to persist the database to a file, you can use the fantastic SQL command that saves an in-memory database to a file:
VACUUM INTO "/path/to/file.sqlite"; - All threads must use the same channel name (in the example,
tasks).
You need to create an sqlite database before the new Runtime