Swoole 快速起步:协程

PHP 语言不支持多线程,因此 Swoole 使用多进程模式。

在多进程模式下存在进程内存隔离,解决方案就是使用 MySQL、MongoDB、Redis 等外部存储服务。

PHP 提供的 MySQLCURLRedis 等客户端是同步的,会导致服务器程序发生阻塞。

Swoole 提供了常用的异步客户端组件,来解决此问题。编写纯异步服务器程序时,可以使用这些异步客户端。

在最新的Swoole 4.x 版本中移除了这些异步模块,将使用协程客户端代替。

使用协程可以以传统同步编程的方法编写代码,底层自动切换为异步IO,既保证了编程的简单性,又可借助异步IO

使用协程客户端

$http = new swoole_http_server("0.0.0.0", 9501);

$http->on('request', function ($request, $response) {
    $db = new Swoole\Coroutine\MySQL();
    $db->connect([
        'host' => '127.0.0.1',
        'port' => 3306,
        'user' => 'user',
        'password' => 'pass',
        'database' => 'test',
    ]);
    $data = $db->query('select * from test_table');
    $response->end(json_encode($data));
});

$http->start();

上面的代码编写与同步阻塞模式的程序完全一致的。但是底层自动进行了协程切换处理,变为异步IO

协程:并发 shell_exec

PHP程序中经常需要用shell_exec执行一些命令,而普通的shell_exec是阻塞的,如果命令执行时间过长,那可能会导致进程完全卡住。 在Swoole4协程环境下可以用Co::exec并发地执行很多命令。

并发 shell_exec

<?php
$c = 10;
while($c--) {
    go(function () {
        //这里使用 sleep 5 来模拟一个很长的命令
        co::exec("sleep 5");
    });
}

执行结果:

$ time php t.php

real    0m5.089s
user    0m0.067s
sys 0m0.038s

 阻塞代码

<?php
$c = 10;
while($c--) {
    //这里使用 sleep 5 来模拟一个很长的命令
    shell_exec("sleep 5");
}

执行结果:

$ time php s.php 

real    0m50.119s
user    0m0.066s
sys 0m0.058s

协程:Go

Swoole4PHP语言提供了强大的CSP协程编程模式。底层提供了3个关键词,可以方便地实现各类功能。

  • go :创建一个协程
  • chan :创建一个通道
  • defer :延迟任务,在协程退出时执行,先进后出

3个功能底层现全部为内存操作,没有任何IO资源消耗。就像PHPArray一样是非常廉价的。如果有需要就可以直接使用。这与socketfile操作不同,后者需要向操作系统申请端口和文件描述符,读写可能会产生阻塞的IO等待。

顺序执行

function test1() 
{
    sleep(1);
    echo "b";
}

function test2() 
{
    sleep(2);
    echo "c";
}

test1();
test2();

结果:

$ time php b1.php
bc
real    0m3.080s
user    0m0.016s
sys     0m0.063s

上述代码中,test1test2会顺序执行,需要3秒才能执行完成。

使用go创建协程

Swoole\Runtime::enableCoroutine();

go(function () 
{
    sleep(1);
    echo "b";
});

go(function () 
{
    sleep(2);
    echo "c";
});

结果: 

$ time php co.php
bc
real    0m2.076s
user    0m0.000s
sys     0m0.078s

可以看到这里只用了2秒就执行完成了。顺序执行耗时等于所有任务执行耗时的总和 :t1+t2+t3...,并发执行耗时等于所有任务执行耗时的最大值 :max(t1, t2, t3, ...);

协程通信: Chan

协程并发执行,另外一个协程,需要依赖这两个协程的执行结果,如果解决此问题呢?

答案就是使用通道(Channel),在Swoole4协程中使用new chan就可以创建一个通道。通道可以理解为自带协程调度的队列。它有两个接口pushpop

  • push:向通道中写入内容,如果已满,它会进入等待状态,有空间时自动恢复
  • pop:从通道中读取内容,如果为空,它会进入等待状态,有数据时自动恢复

使用通道实现并发管理:

$chan = new chan(2);

# 协程1
go (function () use ($chan) {
    $result = [];
    for ($i = 0; $i < 2; $i++)
    {
        $result += $chan->pop();
    }
    var_dump($result);
});

# 协程2
go(function () use ($chan) {
   $cli = new Swoole\Coroutine\Http\Client('www.qq.com', 80);
       $cli->set(['timeout' => 10]);
       $cli->setHeaders([
       'Host' => "www.qq.com",
       "User-Agent" => 'Chrome/49.0.2587.3',
       'Accept' => 'text/html,application/xhtml+xml,application/xml',
       'Accept-Encoding' => 'gzip',
   ]);
   $ret = $cli->get('/');
   // $cli->body 响应内容过大,这里用 Http 状态码作为测试
   $chan->push(['www.qq.com' => $cli->statusCode]);
});

# 协程3
go(function () use ($chan) {
   $cli = new Swoole\Coroutine\Http\Client('www.163.com', 80);
   $cli->set(['timeout' => 10]);
   $cli->setHeaders([
       'Host' => "www.163.com",
       "User-Agent" => 'Chrome/49.0.2587.3',
       'Accept' => 'text/html,application/xhtml+xml,application/xml',
       'Accept-Encoding' => 'gzip',
   ]);
   $ret = $cli->get('/');
   // $cli->body 响应内容过大,这里用 Http 状态码作为测试
   $chan->push(['www.163.com' => $cli->statusCode]);
});

执行结果:

htf@LAPTOP-0K15EFQI:~/swoole-src/examples/5.0$ time php co2.php
array(2) {
  ["www.qq.com"]=>
  int(302)
  ["www.163.com"]=>
  int(200)
}

real    0m0.268s
user    0m0.016s
sys     0m0.109s
htf@LAPTOP-0K15EFQI:~/swoole-src/examples/5.0$

这里使用go创建了3个协程,协程2和协程3分别请求qq.com163.com主页。协程1需要拿到Http请求的结果。这里使用了chan来实现并发管理。

  • 协程1循环两次对通道进行pop,因为队列为空,它会进入等待状态
  • 协程2和协程3执行完成后,会push数据,协程1拿到了结果,继续向下执行

延迟任务:defer

在协程编程中,可能需要在协程退出时自动执行一些任务,做清理工作。类似于PHPregister_shutdown_function

Swoole4中使用defer实现:

Swoole\Runtime::enableCoroutine();

go(function () {
    echo "a";
    defer(function () {
        echo "~a";
    });
    echo "b";
    defer(function () {
        echo "~b";
    });
    sleep(1);
    echo "c";
});

执行结果:

$ time php defer.php
abc~b~a
real    0m1.068s
user    0m0.016s
sys     0m0.047s
htf@LAPTOP-0K15EFQI:~/swoole-src/examples/5.0$

协程:实现 Go 语言风格的 defer

由于Go语言没有提供析构方法,而PHP对象有析构函数,使用__destruct就可以实现Go的风格defer

实现代码:

class DeferTask
{
    private $tasks;

    function add(callable $fn)
    {
        $this->tasks[] = $fn;
    }

    function __destruct()
    {
        //反转
        $tasks = array_reverse($this->tasks);
        foreach($tasks as $fn)
        {
            $fn();
        }
    }
}
  • 基于PHP对象析构方法实现的defer更灵活,如果希望改变执行的时机,甚至可以将DeferTask对象赋值给其他生命周期更长的变量,defer任务的执行可以延长生命周期
  • 默认情况下与Godefer完全一致,在函数退出时自动执行

使用实例

function test() {
    $o = new DeferTask();
    //逻辑代码
    $o->add(function () {
        //code 1
    });
    $o->add(function () {
        //code 2
    });
    //函数结束时,对象自动析构,defer 任务自动执行
    return $retval;
}

官网原文地址:https://wiki.swoole.com/wiki/page/1005.html

发布了46 篇原创文章 · 获赞 42 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/Phplayers/article/details/101755308