Swoole协程处理图片未达异步效果,如何实现真正异步?
图片缩放与格式转换的Swoole协程异步处理问题
问题场景与代码
我用Swoole协程编写了图片缩放并转换为WebP格式的代码,但执行时任务是串行逐个处理的,10张图片的耗时和不使用协程时完全一致。尝试过用Swoole\Process,但无法在request回调内正常使用。
原始代码
<?php use Swoole\Coroutine; use Swoole\Coroutine\System; use Swoole\Coroutine\WaitGroup; use Swoole\Http\Server as HttpServer; use Swoole\Runtime; Runtime::enableCoroutine(true); ini_set('memory_limit', -1); $server = new HttpServer('0.0.0.0', 9000); $server->set([ 'package_max_length' => 10000000, ]); $server->on('start', function ($server) { echo "Server started at http://localhost:9000\n"; }); const SIZES = [ [ 'width' => 400, 'height' => 300, 'name' => '-tile', ], [ 'width' => 1080, 'height' => 720, 'name' => '-big-tile', ] ]; $server->on('request', function ($request, $response) { if ($request->server['request_method'] === 'POST') { $images = $request->files['images']; $wg = new WaitGroup(); foreach ($images as $key => $image) { $filename = pathinfo($image['name'], PATHINFO_FILENAME); switch ($image['type']) { case 'image/png': $sourceImage = imagecreatefrompng($image['tmp_name']); break; case 'image/jpeg': $sourceImage = imagecreatefromjpeg($image['tmp_name']); break; case 'image/webp': $sourceImage = imagecreatefromwebp($image['tmp_name']); break; default: throw new Exception('Unsupported image format: ' . $image['type']); } $wg->add(); $size = SIZES[0]; go(function () use ($wg, $key, $size, $sourceImage, $filename) { echo "BEGIN: $key\n"; try { $sourceWidth = imagesx($sourceImage); $sourceHeight = imagesy($sourceImage); $aspectRatio = $sourceWidth / $sourceHeight; if ($aspectRatio > $size['width'] / $size['height']) { $size['height'] = intval($size['width'] / $aspectRatio); } else { $size['width'] = intval($size['height'] * $aspectRatio); } $targetImage = imagescale($sourceImage, $size['width'], $size['height']); // convert to WebP and save to file ob_start(); imagewebp($targetImage, null, 100); $webpImage = ob_get_clean(); System::writeFile("$filename.webp", $webpImage); imagedestroy($sourceImage); imagedestroy($targetImage); echo "END: $key\n"; } catch (Exception $e) { echo "Error processing image: $filename - " . $e->getMessage() . "\n"; } $wg->done(); }); } $wg->wait(); $response->header('Content-Type', 'text/plain'); $response->end("Images processed\n"); } }); $server->start();
执行输出
BEGIN: 0 END: 0 BEGIN: 1 END: 1 BEGIN: 2 END: 2 BEGIN: 3 END: 3 BEGIN: 4 END: 4 BEGIN: 5 END: 5 BEGIN: 6 END: 6 BEGIN: 7 END: 7
问题根源
当前代码看似用了协程但实际串行执行,核心原因是:
- GD库的所有图片处理函数(
imagecreatefromxxx、imagescale、imagewebp等)都是CPU密集型的同步阻塞操作 - Swoole协程的调度依赖IO事件触发,CPU密集型操作会持续占用当前协程的执行时间片,无法切换到其他协程,导致所有任务只能逐个执行
解决方案
方案1:使用Swoole协程线程池(Coroutine\Pool)
线程池可以让CPU密集型任务在多个线程中并行执行,协程负责调度线程,避免阻塞主进程。
修改后的代码示例:
<?php use Swoole\Coroutine; use Swoole\Coroutine\Pool; use Swoole\Coroutine\System; use Swoole\Http\Server as HttpServer; use Swoole\Runtime; Runtime::enableCoroutine(true); ini_set('memory_limit', -1); $server = new HttpServer('0.0.0.0', 9000); $server->set([ 'package_max_length' => 10000000, ]); // 初始化线程池,建议大小等于CPU核心数 $threadPool = new Pool(4); $server->on('start', function ($server) { echo "Server started at http://localhost:9000\n"; }); const SIZES = [ [ 'width' => 400, 'height' => 300, 'name' => '-tile', ], [ 'width' => 1080, 'height' => 720, 'name' => '-big-tile', ] ]; $server->on('request', function ($request, $response) use ($threadPool) { if ($request->server['request_method'] === 'POST') { $images = $request->files['images']; $total = count($images); $processed = 0; foreach ($images as $key => $image) { $tmpPath = $image['tmp_name']; $filename = pathinfo($image['name'], PATHINFO_FILENAME); $imageType = $image['type']; $size = SIZES[0]; // 投递任务到线程池 $threadPool->push(function () use ($key, $tmpPath, $filename, $imageType, $size, &$processed, $total, $response) { echo "BEGIN: $key\n"; try { // 在线程内重新创建GD资源(线程间不能共享资源) switch ($imageType) { case 'image/png': $sourceImage = imagecreatefrompng($tmpPath); break; case 'image/jpeg': $sourceImage = imagecreatefromjpeg($tmpPath); break; case 'image/webp': $sourceImage = imagecreatefromwebp($tmpPath); break; default: throw new Exception('Unsupported image format: ' . $imageType); } $sourceWidth = imagesx($sourceImage); $sourceHeight = imagesy($sourceImage); $aspectRatio = $sourceWidth / $sourceHeight; if ($aspectRatio > $size['width'] / $size['height']) { $size['height'] = intval($size['width'] / $aspectRatio); } else { $size['width'] = intval($size['height'] * $aspectRatio); } $targetImage = imagescale($sourceImage, $size['width'], $size['height']); ob_start(); imagewebp($targetImage, null, 100); $webpImage = ob_get_clean(); System::writeFile("$filename.webp", $webpImage); imagedestroy($sourceImage); imagedestroy($targetImage); echo "END: $key\n"; } catch (Exception $e) { echo "Error processing image: $filename - " . $e->getMessage() . "\n"; } // 所有任务完成后返回响应 if (++$processed === $total) { $response->header('Content-Type', 'text/plain'); $response->end("Images processed\n"); } }); } } }); $server->start();
方案2:使用Swoole Task进程池
利用Swoole的TaskWorker进程池,将图片处理任务投递到独立的Task进程中并行执行,完全隔离CPU密集型操作对主进程的影响。
修改后的代码示例:
<?php use Swoole\Coroutine; use Swoole\Coroutine\System; use Swoole\Http\Server as HttpServer; use Swoole\Runtime; Runtime::enableCoroutine(true); ini_set('memory_limit', -1); $server = new HttpServer('0.0.0.0', 9000); $server->set([ 'package_max_length' => 10000000, 'task_worker_num' => 4, // 设置Task进程数,建议等于CPU核心数 ]); $server->on('start', function ($server) { echo "Server started at http://localhost:9000\n"; }); const SIZES = [ [ 'width' => 400, 'height' => 300, 'name' => '-tile', ], [ 'width' => 1080, 'height' => 720, 'name' => '-big-tile', ] ]; $server->on('request', function ($request, $response) use ($server) { if ($request->server['request_method'] === 'POST') { $images = $request->files['images']; $total = count($images); $processed = 0; foreach ($images as $key => $image) { // 封装任务数据,传递临时文件路径而非GD资源 $taskData = [ 'tmp_path' => $image['tmp_name'], 'filename' => pathinfo($image['name'], PATHINFO_FILENAME), 'type' => $image['type'], 'size' => SIZES[0], 'key' => $key, ]; // 投递任务到Task进程 $server->task($taskData, function ($server, $taskId, $result) use (&$processed, $total, $response) { // 任务完成回调 if (++$processed === $total) { $response->header('Content-Type', 'text/plain'); $response->end("Images processed\n"); } }); } } }); // Task进程处理逻辑 $server->on('task', function ($server, $taskId, $fromWorkerId, $data) { echo "BEGIN: {$data['key']}\n"; try { switch ($data['type']) { case 'image/png': $sourceImage = imagecreatefrompng($data['tmp_path']); break; case 'image/jpeg': $sourceImage = imagecreatefromjpeg($data['tmp_path']); break; case 'image/webp': $sourceImage = imagecreatefromwebp($data['tmp_path']); break; default: throw new Exception('Unsupported image format: ' . $data['type']); } $sourceWidth = imagesx($sourceImage); $sourceHeight = imagesy($sourceImage); $aspectRatio = $sourceWidth / $sourceHeight; $size = $data['size']; if ($aspectRatio > $size['width'] / $size['height']) { $size['height'] = intval($size['width'] / $aspectRatio); } else { $size['width'] = intval($size['height'] * $aspectRatio); } $targetImage = imagescale($sourceImage, $size['width'], $size['height']); ob_start(); imagewebp($targetImage, null, 100); $webpImage = ob_get_clean(); System::writeFile("{$data['filename']}.webp", $webpImage); imagedestroy($sourceImage); imagedestroy($targetImage); echo "END: {$data['key']}\n"; } catch (Exception $e) { echo "Error processing image: {$data['filename']} - " . $e->getMessage() . "\n"; } // 返回任务结果(可自定义内容) return true; }); $server->start();
注意事项
- 线程池和Task进程池的大小建议设置为与服务器CPU核心数一致,避免过度调度导致性能下降
- 不要在线程/进程之间共享GD资源,必须在每个任务内部重新创建和销毁资源
- 如果需要处理大量图片,建议配合消息队列实现异步任务解耦,避免请求超时
内容的提问来源于stack exchange,提问作者Vitalik Tarnavskiy
相关产品推荐
相关产品推荐

