zoukankan      html  css  js  c++  java
  • swoole深入学习 8. 协程 转

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
    本文链接:https://blog.csdn.net/yangyi2083334/article/details/80009135

    swoole深入学习 8. 协程

    swoole 在 2.0正式版加入了协程功能。这一章主要来深究一下在Swoole中如何使用协程。

    什么是协程?

    协程(Coroutine)也叫用户级线程, 很多人分不清楚协程和线程和进程的关系。进程(Process)是操作系统分配资源的单位,线程(Thread)是进程的一个实体,是CPU调度和分派的基本单位。线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。简单的说就是: 线程和进程的调度是由操作系统来调控, 而协程的调度由用户自己调控。 所以协程调度器可以在协程A即将进入阻塞IO操作, 比如 socket 的 read (其实已经设置为异步IO )之前, 将该协程挂起,把当前的栈信息 StackA 保存下来, 然后切换到协程B, 等到协程A的该 IO操作返回时, 再根据 StackA 切回到之前的协程A当时的状态。协程相对于事件驱动是一种更先进的高并发解决方案, 把复杂的逻辑和异步都封装在底层, 让程序员在编程时感觉不到异步的存在, 用响马的话就是【用同步抒写异步情怀】。

    所以,你可以理解为协程就是用同步的方式来写异步回调的高并发程序。

    值得注意的是:swoole协程与线程不同,在一个进程内创建的多个协程,实际上是串行的。同一CPU时间,只有一个协程在执行,因此swoole协程是阻塞运行的,语法也是用的同步的方式在写,只不过是在底层做了切换调度,提高的仅仅是单个进程接收请求的能力,并没有提高执行速度(总共需要的时间)

    所以协程最大的功能就是提高了单个进程接受请求的能力,进而提高了总体高并发的能力。

    swoole 支持的协程客户端

    目前在swoole中支持的协程用的较多的有以下:

    SwooleCoroutineClient
    SwooleCoroutineRedis
    SwooleCoroutineMySQL
    SwooleCoroutineHttpClient
    SwooleCoroutinePostgreSQL
    SwooleCoroutineHTTP2Client

    我也会针对这些协成做一一讲解。

    server中支持协程的回调方法列表

    目前Swoole2仅有部分事件回调函数底层自动创建了协程,以下回调函数可以调用协程客户端 (文本用基于swoole 2.1.3版本):

    1. swooleserver 下面的:

    onWorkerStart
    onClose
    onConnect
    onReceive
    onPacket

    2. swoolewebsocketserver 下面的

    onMessage
    onHandShake
    onOpen

    3. swoolehttpserver 下面的

    onRequest

    4. tick/after 定时器

    及时的跟新请看官网:https://wiki.swoole.com/wiki/page/696.html

    在新版本的中,在不支持协程的位置可以使用goCo::create创建协程。这些内容我会在下节中会单独讲。

    SwooleCoroutineClient

    SwooleCoroutineClient 提供了TCP和UDP传输协议Socket客户端的封装代码,使用时仅需new SwooleCoroutineClient即可。

    直接看例子吧,我在swoolehttpserveronRequest里去调用tcp client协程:

    <?php
    $server = new SwooleHttpServer("127.0.0.1", 9502, SWOOLE_BASE);
    
    $server->set([
        'worker_num' => 1,
    ]);
    
    $server->on('Request', function ($request, $response) {
    
    
        //屏蔽Google浏览器发的favicon.ico请求
        if ($request->server['path_info'] == '/favicon.ico' || $request->server['request_uri'] == '/favicon.ico') {
            return $response->end();
        }
        
        var_dump('stime:' . microtime(true));
    
        $client = new SwooleCoroutineClient(SWOOLE_SOCK_TCP);
        
        var_dump('new:' . microtime(true));
        
        //connect的三个参数: ip, 端口, 超时时间。
        //超时时间单位是秒s,支持浮点数。默认为0.1s,即100ms,超时发生时,连接会被自动close掉。
        if (!$client->connect('127.0.0.1', 9501, 0.5)) {
            return $response->end(' swoole  response error:' . $client->errCode);
        }
    
        var_dump('connect:' . microtime(true));
    
        // send 发送数据给server ,内容为字符串
        $client->send("hello world
    ");
    
        var_dump('send:' . microtime(true));
    
    
        // recv 接收数据 参数为超时时间,如果不设置以connect的为准。超时会自动close掉。
        echo "from server: " $client->recv(5);
    
        var_dump('recv:' . microtime(true));
    
    
        //close 关闭连接
        $client->close();
    
        $response->end('ok');
    });
    $server->start();
     

     

     

    运行一下,然后在浏览器访问127.0.0.1:9502

    string(21) "stime:1524051524.1339"
    string(19) "new:1524051524.1343"
    string(23) "connect:1524051524.1355"
    string(20) "send:1524051524.1355"
    from server: hello, 0
    string(20) "recv:1524051528.1374"
     

     

     

    通过打印时间,可以看出:conncet,send是没有阻塞的。会立即返回。recv是阻塞的,阻塞了4秒,这是因为我测试超时时间,在http server里返回的时候sleep了4秒。

    conncet会切换唤起一次协程,但是是不阻塞的,会立即返回。 recv是 阻塞的,会唤起协程等待数据。send操作是立即返回的,没有协程切换。上面完全用同步的方式,来写异步阻塞回调,很流畅。

    SwooleCoroutineHttpClient

    这个是http的协程客户端,与swoolehttp client异步客户端的用法是一样的,都是异步的,只不过用到了协程切换机制,不需要写回调,直接用同步的方式来处理。

    看一个例子,我在 tcp server onworkStart 回调了适用了协程:

      <?php
    
    $serv = new SwooleServer('0.0.0.0', 9503);
    
    //初始化swoole服务
    $serv->set(array(
        'worker_num' => 1,
        'daemonize' => false, //是否作为守护进程,此配置一般配合log_file使用
        'max_request' => 1000,
        'log_file' => './swoole.log',
    //            'task_worker_num' => 8
    ));
    
    //设置监听
    $serv->on('Start', 'onStart');
    $serv->on('Connect', 'onConnect');
    $serv->on("Receive", 'onReceive');
    $serv->on("Close", 'onClose');
    
    $serv->on('WorkerStart', function ($serv, $workerId) {
    
        //创建http client协程
    
        echo $workerId . PHP_EOL;
    
        //new 
        $cli = new SwooleCoroutineHttpClient('127.0.0.1', 9501);
        
        //设置请求头
        $cli->setHeaders([
            'Host' => "localhost",
            "User-Agent" => 'Chrome/49.0.2587.3',
            'Accept' => 'text/html,application/xhtml+xml,application/xml',
            'Accept-Encoding' => 'gzip',
        ]);
        
        //设置超时时间
        $cli->set(['timeout' => 5]);
    
        var_dump('connect:' . microtime(true));
        
        //get方法,协程,会阻塞
        $cli->get('/index.php');
    
        var_dump('get:' . microtime(true));
    
    
        echo $cli->body;
    
        var_dump('body:' . microtime(true));
    
        $cli->close();
    
    });
    
    
    function onStart($serv)
    {
        //echo SWOOLE_VERSION . " onStart
    ";
    }
    
    function onConnect($serv, $fd)
    {
        echo $fd . "Client Connect.
    ";
    }
    
    function onReceive($serv, $fd, $from_id, $data)
    {
        echo "Get Message From Client {$fd}:{$data}
    ";
        // send a task to task worker.
    
        $serv->send($fd, "hello, " . $from_id);
    
    }
    
    function onClose($serv, $fd)
    {
        echo "Client Close.
    ";
    }
    
    
    //开启
    $serv->start();
     

     

     

    会输出:

    0
    string(23) "connect:1524110315.0407"
    string(18) "get:1524110318.044"
    hello!
    string(20) "body:1524110318.0441"
     

     

     

    我再http server里sleep了3秒,可以看出,get就用了3秒的时间,会阻塞住等待。

    SwooleCoroutineRedis

    redis 在平时用的非常多,基本在php中要么用phpredis的C扩展,要么用php语言版本的phpiredis。swoole里面也提供过异步的redis方案,但是由于需要层层回调,很是蛋疼。协程版本的redis就简单的多了。

    需要安装一个第三方的异步Redis库hiredis,并且在编译swoole时增加--enable-coroutine--enable-async-redis来开启此功能。

    直接上代码吧:

     $redis = new SwooleCoroutineRedis();
     $res = $redis->connect('127.0.0.1', 6379);
     $ret = $redis->set('coroutine_i', 50);
     
     //协程唤起,阻塞,但是写程序无感知
     $redis->zAdd('key1', 1, 'val1');
     $redis->zAdd('key1', 0, 'val0');
     $redis->zAdd('key1', 5, 'val5');
      var_dump($redis->zRange('key1', 0, -1, true));
        
     $redis->close();
     

     

     

    打印为:

    array(6) {
      [0]=>
      string(4) "val0"
      [1]=>
      string(1) "0"
      [2]=>
      string(4) "val1"
      [3]=>
      string(1) "1"
      [4]=>
      string(4) "val5"
      [5]=>
      string(1) "5"
    }
     

    和 phpredis的调用方法几乎有一模一样,但是输出的格式会不一样,而且有如下坑:

    1. $redis->get('no-exist-key'), get一个不存在的key 。返回的是 null ,不是 false。
    
    2. $redis->zRevRange('key', 0, 19, true); 获取一个zset集合,结果集会不一样,不是键值对的。
    
    3. redis 的连接,第三个参数是自动php序列化数据,要设置为false,或者不填,默认是false:redis->connect($host, $port, false)。设置为true, 会在zset数据读取出现问题。已知道的坑了。
     

     

     

    尚未实现的Redis命令:

    scan object sort migrate hscan sscan zscan
     

     

    SwooleCoroutineMySQL

    mysql协程,很简单,直接上代码

    <?php
    $server = new SwooleHttpServer("127.0.0.1", 9502, SWOOLE_BASE);
    
    $server->set([
        'worker_num' => 1,
    ]);
    
    $server->on('Request', function ($request, $response) {
    
    
        //屏蔽Google浏览器发的favicon.ico请求
        if ($request->server['path_info'] == '/favicon.ico' || $request->server['request_uri'] == '/favicon.ico') {
            return $response->end();
        }
    
        var_dump('stime:' . microtime(true));
    
        //new mysql
        $db = new SwooleCoroutineMySQL();
    
        var_dump('new:' . microtime(true));
    
        $server = array(
            'host' => '127.0.0.1',
            'user' => 'root',
            'password' => 'root',
            'database' => 'rcs',
        );
        
        //connect
        $ret1 = $db->connect($server);
    
        var_dump('connect:' . microtime(true));
    
        //直接query
        $info = $db->query('SELECT 1+1;');
        var_dump($info);
        
        //1. 先 prepare
        $stmt = $db->prepare('SELECT id,risk_id FROM rcs_result WHERE id=? and risk_id=?');
    
        var_dump('prepare:' . microtime(true));
    
        if ($stmt == false) {
            var_dump($db->errno, $db->error);
        } else {
            // 2. 配合 prepare,再execute
            $ret2 = $stmt->execute(array(71, 60));
            var_dump('execute:' . microtime(true));
    
            var_dump($ret2);
    
            $ret3 = $stmt->execute(array(13, 12));
            var_dump('execute:' . microtime(true));
    
            var_dump($ret3);
        }
    
    
        $response->end('ok');
    
    });
    $server->start();
     

     

    比较简单,就不阐述了,但是可能会有坑,在线上慎用。

    协程并发

    协程其实也是阻塞运行的,如果,在一个执行中,比如同时查redis,再去查mysql,即使用了上面的协程,也是顺序执行的。那么可不可以几个协程并发执行呢?

    答案当然是可以的,需要用延迟收包,当遇到IO 阻塞的时候,协程就挂起了,不会阻塞在那里等着网络回报,而是继续往下走。

    swoole 协程调用里面可以用setDefer()方法声明延迟收包,然后通过recv()方法收包。

    看下面这个例子:

    <?php
    $server = new SwooleHttpServer("127.0.0.1", 9502, SWOOLE_BASE);
    
    $server->set([
        'worker_num' => 1,
    ]);
    
    $server->on('Request', function ($request, $response) {
    
        //屏蔽Google浏览器发的favicon.ico请求
        if ($request->server['path_info'] == '/favicon.ico' || $request->server['request_uri'] == '/favicon.ico') {
            return $response->end();
        }
    
        echo "#BEGIN :" . microtime(true) . PHP_EOL;
        
        // tcp
        $tcpclient = new SwooleCoroutineClient(SWOOLE_SOCK_TCP);
        $tcpclient->connect('127.0.0.1', 9501, 0.5);
        $tcpclient->send("hello world
    ");
    
        echo "#after TCP:" . microtime(true) . PHP_EOL;
    
        //redis
        $redis = new SwooleCoroutineRedis();
        $redis->connect('127.0.0.1', 6379);
        $redis->setDefer();
        $redis->get('key');
    
        echo "#after redis:" . microtime(true) . PHP_EOL;
    
        //mysql
        $mysql = new SwooleCoroutineMySQL();
        $mysql->connect([
            'host' => '127.0.0.1',
            'user' => 'root',
            'password' => 'root',
            'database' => 'rcs',
        ]);
        $mysql->setDefer();
        $b = $mysql->query('select sleep(10)');
        var_dump("mysql, return:", $b);
    
    
        echo "#after MYSQL:" . microtime(true) . PHP_EOL;
    
        //http
        $httpclient = new SwooleCoroutineHttpClient('0.0.0.0', 9599);
        $httpclient->setHeaders(['Host' => "www.qq.com"]);
        $httpclient->set(['timeout' => 1]);
        $httpclient->setDefer();
        $httpclient->get('/');
    
        echo "#after HTTP:" . microtime(true) . PHP_EOL;
    
        //使用recv收报
        $tcp_res = $tcpclient->recv();
        echo "#recv tcp:" . microtime(true) . PHP_EOL;
    
        $redis_res = $redis->recv();
        echo "#recv redis:" . microtime(true) . PHP_EOL;
    
        $mysql_res = $mysql->recv();
        echo "#recv mysql:" . microtime(true) . PHP_EOL;
    
        $http_res = $httpclient->recv();
        echo "#recv http:" . microtime(true) . PHP_EOL;
    
        echo "#finish :" . microtime(true) . PHP_EOL;
    
        $response->end('Test End');
    });
    $server->start();
     

     

     

    我分别在各个点打印了时间点,用来看下执行的时间。打开浏览器

    http://127.0.0.1:9502/
     

     

    看下输出:

    #BEGIN :1524136394.7842
    
    #after TCP:1524136394.7877
    #after redis:1524136394.7909
    #after MYSQL:1524136394.799
    #after HTTP:1524136394.7993
    
    #recv tcp:1524136395.2986
    #recv redis:1524136395.2986
    #recv mysql:1524136404.7898 //阻塞了10秒
    #recv http:1524136404.7898
    
    #FINISH :1524136404.7898
     

     

     

    前面的都是非阻塞调用,在收包的时候就是阻塞了。总共花了:10.0056秒。

    那一摸一样的代码,我们不用延迟收报,看下时间:

    <?php
    $server = new SwooleHttpServer("127.0.0.1", 9502, SWOOLE_BASE);
    
    $server->set([
        'worker_num' => 1,
    ]);
    
    $server->on('Request', function ($request, $response) {
    
    
        //屏蔽Google浏览器发的favicon.ico请求
        if ($request->server['path_info'] == '/favicon.ico' || $request->server['request_uri'] == '/favicon.ico') {
            return $response->end();
        }
    
        echo "#BEGIN :" . microtime(true) . PHP_EOL;
    
        $tcpclient = new SwooleCoroutineClient(SWOOLE_SOCK_TCP);
        $tcpclient->connect('127.0.0.1', 9501, 0.5);
        $tcpclient->send("hello world
    ");
        $tcpclient->recv();
    
        //var_dump("tpc, return:", $tcpclient->recv());
    
        echo "#after TCP:" . microtime(true) . PHP_EOL;
    
    
        $redis = new SwooleCoroutineRedis();
        $redis->connect('127.0.0.1', 6379);
        //$redis->setDefer();
        $a = $redis->get('key');
        var_dump("redis, return:", $a);
    
        echo "#after redis:" . microtime(true) . PHP_EOL;
    
    
        $mysql = new SwooleCoroutineMySQL();
        $mysql->connect([
            'host' => '127.0.0.1',
            'user' => 'root',
            'password' => 'root',
            'database' => 'rcs',
        ]);
        //$mysql->setDefer();
        $b = $mysql->query('select sleep(10)');
        var_dump("mysql, return:", $b);
    
        echo "#after MYSQL:" . microtime(true) . PHP_EOL;
    
    
        $httpclient = new SwooleCoroutineHttpClient('0.0.0.0', 9599);
        $httpclient->setHeaders(['Host' => "www.qq.com"]);
        $httpclient->set(['timeout' => 1]);
        //$httpclient->setDefer();
        $c = $httpclient->get('/');
        var_dump("http, return:", $c);
    
        echo "#after HTTP:" . microtime(true) . PHP_EOL;
    
    
        echo "#FINISH :" . microtime(true) . PHP_EOL;
    
    
        $response->end('Test End');
    });
    $server->start();
     

     

    打印如下:

    #BEGIN :1524136719.7372
    
    #after TCP:1524136720.2381
    
    string(14) "redis, return:" NULL
    #after redis:1524136720.2388
    
    string(14) "mysql, return:"
    array(1) {
      [0]=>
      array(1) {
        ["sleep(10)"]=>
        string(1) "0"
      }
    }
    #after MYSQL:1524136730.237
    
    string(13) "http, return:" bool(false)
    #after HTTP:1524136730.2375
    
    #FINISH :1524136730.2375
     

     

     

    花费时间为:10.5003秒。

    好吧。比同步阻塞协程快了0.5秒。 多运行几次,发现快不了多少,因为是单个进程内的协程也是串行的。

    总结

    swoole 协程只是单纯的让异步代码用同步的方式来写,并没有提高一次进程内cgi 请求的执行速度,提高的是整个进程接受请求的能力,提高整体的qps。

  • 相关阅读:
    10个实用的但偏执的Java编程技术
    分布式文件系统FastDFS设计原理
    👮 Golang Gin/Ace/Iris/Echo RBAC 鉴权库
    深入浅出爬虫之道: Python、Golang与GraphQuery的对比
    (.*?)
    开箱即用的高匿代理抓取工具
    6.可见性关键字(volidate)
    5.同步关键字(synchronized)
    4.等待和通知
    3.线程优先级
  • 原文地址:https://www.cnblogs.com/brady-wang/p/11512408.html
Copyright © 2011-2022 走看看