You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 16:24:55