It may seem like PHP + Threads + ThreadPool isn’t anything special. After all, we already have Parallel. But what’s coming this summer is another small step toward a completely new and unfamiliar PHP, a PHP that can solve modern problems more efficiently and with cleaner code.
A lot of things are happening here for the first time:
• Seamless combination of threads and coroutines. The thread creation function is called spawn_thread for a reason, hinting that the returned object behaves very much like a coroutine. This means it can be composed with await, await_all().
• New ABI for object copying. After a lot of hesitation, I added a dedicated method directly into the Zend Object ABI for copying objects between threads. Resources received something similar.
• Object movement like in Rust. We talked about this a year ago, that PHP might adopt move semantics. And here it is. FutureState can move itself between threads.
• WeakReference is handled correctly when crossing thread boundaries.
Can you feel how this opens the door to cleaner code without hacks? Things that developers used to simulate with processes, Redis, Python helpers, or massive libraries can now be done in just 10–20 lines of code.
<?php
use Async\ThreadPool;
use function Async\spawn;
// This function is executed in a worker thread.
// GD operations are CPU-bound — exactly the kind of tasks that benefit from threads.
function processImage(string $src, string $outDir, int $maxWidth): array
{
$info = getimagesize($src);
if ($info === false) {
throw new \RuntimeException("Failed to read: $src");
}
// Open source
$original = match ($info[2]) {
IMAGETYPE_JPEG => imagecreatefromjpeg($src),
IMAGETYPE_PNG => imagecreatefrompng($src),
IMAGETYPE_WEBP => imagecreatefromwebp($src),
default => throw new \RuntimeException("Unsupported format: $src"),
};
// Resize while preserving aspect ratio
[$origW, $origH] = [$info[0], $info[1]];
$scale = min(1.0, $maxWidth / $origW);
$newW = (int) ($origW * $scale);
$newH = (int) ($origH * $scale);
$resized = imagescale($original, $newW, $newH, IMG_BICUBIC);
imagedestroy($original);
// Convert to grayscale
imagefilter($resized, IMG_FILTER_GRAYSCALE);
// Save to output directory
$outPath = $outDir . '/' . basename($src, '.' . pathinfo($src, PATHINFO_EXTENSION)) . '_thumb.jpg';
imagejpeg($resized, $outPath, quality: 85);
$outSize = filesize($outPath);
imagedestroy($resized);
return [
'src' => $src,
'out' => $outPath,
'size_kb' => round($outSize / 1024, 1),
'width' => $newW,
'height' => $newH,
];
}
spawn(function() {
$srcDir = '/var/www/uploads/originals';
$outDir = '/var/www/uploads/thumbs';
$maxW = 800;
// List of files to process
$files = glob("$srcDir/*.{jpg,jpeg,png,webp}", GLOB_BRACE);
if (empty($files)) {
echo "No files to process\n";
return;
}
$pool = new ThreadPool(workers: (int) shell_exec('nproc') ?: 4);
// map() preserves order — results[i] corresponds to files[i]
$results = $pool->map($files, fn(string $path) => processImage($path, $outDir, $maxW));
$totalKb = 0;
foreach ($results as $r) {
echo sprintf("%-40s → %s (%dx%d, %.1f KB)\n",
basename($r['src']), basename($r['out']),
$r['width'], $r['height'], $r['size_kb']
);
$totalKb += $r['size_kb'];
}
echo sprintf("\nProcessed: %d files, total %.1f KB\n", count($results), $totalKb);
$pool->close();
});
Yes, version 0.7.0 isn’t released yet, and there is still time to change or refine things. The most important open question is move semantics. How to make it clear in code that an object is being moved. Should the syntax reflect that?
Join the discussion about the new version. Your knowledge and experience can be valuable!
Top comments (2)
Coroutines and moving objects across threads Rust-style in PHP is genuinely exciting! This could open up a whole new class of high-performance PHP applications without switching languages.
Exactly. And there will be no need to use Go or Python.