Swoole协程使用示例

<?php
declare(strict_types=1);
PHP_SAPI !== 'cli' && exit('脚本只能在命令行执行');
set_time_limit(0);
ini_set('memory_limit', '-1');
date_default_timezone_set('Asia/Shanghai');

$start = microtime(true); // 程序开始执行时间

$list = [];

// 使用Swoole\Coroutine\run()创建协程容器
Swoole\Coroutine\run(static function () use (&$list): void {
    $num = 5; // 协程任务中for循环次数

    // 使用Swoole\Coroutine::create()创建协程并立即执行
    Swoole\Coroutine::create(static function () use ($num, &$list): void {
        for ($i = 1; $i <= $num; $i++) {
            Swoole\Coroutine\System::sleep(1); // 休眠1秒
            echo "协程1 ---> \$i = $i" . PHP_EOL;
            $list[] = "协程1 ---> \$i = $i";
        }
    });
    echo '----- 第1个协程已创建并执行 -----' . PHP_EOL;

    // 使用Swoole\Coroutine::create()创建协程并立即执行
    Swoole\Coroutine::create(static function () use ($num, &$list): void {
        for ($j = 1; $j <= $num; $j++) {
            Swoole\Coroutine\System::sleep(1); // 休眠1秒
            echo "协程2 ---> \$j = $j" . PHP_EOL;
            $list[] = "协程2 ---> \$j = $j";
        }
    });
    echo '----- 第2个协程已创建并执行 -----' . PHP_EOL;
});
echo '----- 协程容器内的协程任务已全部执行完 -----' . PHP_EOL;

// 计算程序执行耗时
$end = microtime(true); // 程序执行完毕时间
$exe = number_format(($end - $start), 6);
echo "程序执行耗时:{$exe}秒" . PHP_EOL;

echo '$list = ' . var_export($list, true) . PHP_EOL;


// ----- 第1个协程已创建并执行 -----
// ----- 第2个协程已创建并执行 -----
// 协程1 ---> $i = 1
// 协程2 ---> $j = 1
// 协程2 ---> $j = 2
// 协程1 ---> $i = 2
// 协程1 ---> $i = 3
// 协程2 ---> $j = 3
// 协程2 ---> $j = 4
// 协程1 ---> $i = 4
// 协程1 ---> $i = 5
// 协程2 ---> $j = 5
// ----- 协程容器内的协程任务已全部执行完 -----
// 程序执行耗时:5.014917秒
// $list = array (
//   0 => '协程1 ---> $i = 1',
//   1 => '协程2 ---> $j = 1',
//   2 => '协程2 ---> $j = 2',
//   3 => '协程1 ---> $i = 2',
//   4 => '协程1 ---> $i = 3',
//   5 => '协程2 ---> $j = 3',
//   6 => '协程2 ---> $j = 4',
//   7 => '协程1 ---> $i = 4',
//   8 => '协程1 ---> $i = 5',
//   9 => '协程2 ---> $j = 5',
// )


//========== 总结 ==========//
// 1、协程必须在协程容器里创建,不要在协程里使用sleep()函数,而是使用Swoole\Coroutine\System::sleep()来替代。
// 2、从输出结果可以知道,协程容器里的协程是并行执行的,协程与协程之间并不会发生阻塞,但是整个协程容器本身是会发生阻塞的,
//    它必须等容器内部的所有协程任务执行完,才会继续执行容器后面的代码。



  使用协程和不用协程发起远程请求的耗时对比实验

<?php
declare(strict_types=1);
PHP_SAPI !== 'cli' && exit('脚本只能在命令行执行');
set_time_limit(0);
ini_set('memory_limit', '-1');
date_default_timezone_set('Asia/Shanghai');

/**
 * 从接口响应内容中解析出股票名称
 *
 * @param string $contents 接口响应内容
 * @return string 股票名称
 */
function get_name_from_contents(string $contents): string
{
    $name = '';

    $contents = iconv('GBK', 'UTF-8', $contents);

    $count = preg_match_all('/v_.*?=".*?~(.*?)~/is', $contents, $matches);
    if ($count === 1 && isset($matches[1][0])) {
        $name = $matches[1][0];
    }

    return $name;
}

$api = 'https://qt.gtimg.cn'; // 接口地址

// 股票代码
$codes = [
    'sh600519', // 贵州茅台
    'sz300896', // 爱美客
    'sh603444', // 吉比特
    'sz000596', // 古井贡酒
    'sh600436', // 片仔癀
    'sz300760', // 迈瑞医疗
    'sh600809', // 山西汾酒
    'sz002594', // 比亚迪
    'sz002371', // 北方华创
    'sz000568', // 泸州老窖
    'sz300750', // 宁德时代
    'sh605499', // 东鹏饮料
    'sh603290', // 斯达半导
    'sz000858', // 五粮液
    'sz300573', // 兴齐眼药
    'sz300033', // 同花顺
    'sh601799', // 星宇股份
    'sz002821', // 凯莱英
    'sz002920', // 德赛西威
    'sh603129', // 春风动力
    'sz000661', // 长春高新
    'sz002304', // 洋河股份
    'sh600702', // 舍得酒业
    'sh603345', // 安井食品
    'sz301367', // 怡和嘉业
];


///////////////////////// 使用协程 /////////////////////////


$start = microtime(true); // 程序开始执行时间

$list1 = [];

// 使用Swoole\Coroutine\run()创建协程容器
Swoole\Coroutine\run(static function () use ($api, $codes, &$list1): void {
    foreach ($codes as $code) {
        // 使用Swoole\Coroutine::create()创建协程并立即执行
        Swoole\Coroutine::create(static function () use ($api, $code, &$list1): void {
            $contents = file_get_contents("$api/q=$code");
            $list1[$code] = get_name_from_contents($contents);
        });
    }
});

// 计算程序执行耗时
$end = microtime(true); // 程序执行完毕时间
$exe = number_format(($end - $start), 6);
echo "程序执行耗时(使用协程):{$exe}秒" . PHP_EOL;

echo '$list1 = ' . var_export($list1, true) . PHP_EOL;


///////////////////////// 不用协程 /////////////////////////


$start = microtime(true); // 程序开始执行时间

$list2 = [];

foreach ($codes as $code) {
    $contents = file_get_contents("$api/q=$code");
    $list2[$code] = get_name_from_contents($contents);
}

// 计算程序执行耗时
$end = microtime(true); // 程序执行完毕时间
$exe = number_format(($end - $start), 6);
echo "程序执行耗时(不用协程):{$exe}秒" . PHP_EOL;

echo '$list2 = ' . var_export($list2, true) . PHP_EOL;


//========== 总结 ==========//
// 1、$list1(使用协程)和$list2(不用协程)的数组元素顺序可能是不同的,因为使用协程请求API是并行的,无法确定哪个请求先完成。
// 2、并不是每次使用协程都比不用协程执行耗时短,原因是示例的远程请求次数比较少,如果使用协程时因为网络或API服务器的原因导致有
//    个别请求的时间特别长,就会出现不用协程反而耗时更短的情况,但如果多次试验总体来看还是使用协程耗时更短占多数。



HTTP服务器(协程风格)

<?php
declare(strict_types=1);
ini_set('display_errors', 'On');
ini_set('error_reporting', E_ALL);
ini_set('max_execution_time', 0);
ini_set('memory_limit', '-1');
PHP_SAPI !== 'cli' && exit('脚本只能在命令行执行');

/**
 * 获取请求参数
 *
 * @param Swoole\Http\Request $request Swoole\Http\Request对象
 * @param string $name 请求参数名称,示例:get、post、cookie、server、……
 * @return array 请求参数(键值对),若参数不存在则返回空数组
 */
function request(Swoole\Http\Request $request, string $name): array
{
    switch (strtolower($name)) {
        case 'get':
        {
            $arr = (isset($request->get) && is_array($request->get)) ? $request->get : [];
            break;
        }
        case 'post':
        {
            $arr = (isset($request->post) && is_array($request->post)) ? $request->post : [];
            break;
        }
        case 'cookie':
        {
            $arr = (isset($request->cookie) && is_array($request->cookie)) ? $request->cookie : [];
            break;
        }
        case 'server':
        {
            $arr = (isset($request->server) && is_array($request->server)) ? $request->server : [];
            break;
        }
        default:
        {
            $arr = [];
        }
    }

    return $arr;
}

Swoole\Coroutine::set(['hook_flags' => SWOOLE_HOOK_ALL]); // 一键协程化

Swoole\Coroutine\run(function () {
    $server = new Swoole\Coroutine\Http\Server('localhost', 9502, false);

    $server->handle('/', function (Swoole\Http\Request $request, Swoole\Http\Response $response): void {
        defer(static function (): void {
            echo 'HTTP请求处理完成……' . PHP_EOL;
        });

        // 处理“/favicon.ico”请求
        if ($request->server['request_uri'] === '/favicon.ico') {
            $response->setStatusCode(404);
            $response->end('404 Not Found');
            return;
        }

        echo 'e.g.1 当前协程ID:' . Swoole\Coroutine::getCid() . PHP_EOL; // 获取当前协程ID,若当前不在协程环境中则返回-1

        // 创建协程处理HTTP请求(通常无需再次创建协程,这里只是出于演示如何使用协程所以创建)
        go(static function () use ($request, $response): void {
            echo 'e.g.2 当前协程ID:' . Swoole\Coroutine::getCid() . PHP_EOL; // 获取当前协程ID,若当前不在协程环境中则返回-1

            defer(static function (): void {
                echo 'defer()执行中……' . PHP_EOL;
            });

            $_GET = request($request, 'get');
            $_POST = request($request, 'post');
            $_COOKIE = request($request, 'cookie');
            $_SERVER = request($request, 'server');
            // print_r($_GET); //
            // print_r($_POST); //
            // print_r($_COOKIE); //
            // print_r($_SERVER); //

            $uri = $_SERVER['request_uri'] ?? '';
            $response->end($uri);
        });
    });

    $server->start(); // 启动HTTP服务器
});


//========== 总结 ==========//
// 1、Swoole对于每个HTTP请求都会自动创建协程处理,所以在回调函数里不需要再次创建协程。
// 2、协程之间的数据是隔离的(不管是并列还是嵌套),所以使用全局数据时要特别注意,通常只有类似系统配置这样数据需要全局化,
//    因为这类数据是所有用户共用的,常见做法是把系统配置保存到类的静态属性中(相当于缓存),这样在代码任何地方都可以使用。

Copyright © 2024 码农人生. All Rights Reserved