swoole学习之: 执行异步任务

本文参考自 https://wiki.swoole.com/#/start/start_task, 内容有调整

执行异步任务 (Task)

在 Server 程序中如果需要执行很耗时的操作,比如一个聊天服务器发送广播,Web 服务器中发送邮件/短信。如果直接去执行这些函数就会阻塞当前进程,导致服务器响应变慢。

Swoole 提供了异步任务处理的功能,可以投递一个异步任务到 TaskWorker 进程池中执行,不影响当前请求的处理速度。

程序代码

基于我们前面的那个 TCP 服务器,只需要增加 onTaskonFinish 2 个事件回调函数即可。另外需要设置 task 进程数量,可以根据任务的耗时和任务量配置适量的 task 进程。

async_task.php

<?php
$server = new Swoole\Server('10.0.2.7', 81);//虚拟机外部访问地址: 127.0.0.1:40181

//设置异步任务的工作进程数量
$server->set([
   'task_worker_num' => 1 //这里为了测试多任务的处理, 特地只设置一个, 实际上我们要根据任务数量设置多个进程
]);

$server->on('Start', function($server){
    echo 'Swoole AsyncTask TCP server started at'.date('Y-m-d H:i:s').PHP_EOL;
});

//监听连接进入事件
$server->on('Connect', function($server, $fd){
    //$fd: 客户端连接的唯一标识, int
    echo date('Y-m-d H:i:s').' Client connected fd='.$fd.PHP_EOL;//在服务器命令行端显示文字
    $server->send($fd, 'Welcome, your ID is: '.$fd.PHP_EOL);
});

//收到任务, 并投递异步任务 (此回调函数在worker进程中执行)
$server->on('Receive', function($server, $fd, $reactor_id, $data){
    //$reactor_id: TCP连接所在的Reactor线程ID, int
    //$data: 收到的数据内容,可能是文本或者二进制内容
    $data = trim($data);
    echo date('Y-m-d H:i:s').' Client '.$fd.' send message: '.$data.PHP_EOL;

    //投递异步任务
    $task_id = $server->task($data);

    $server->send($fd, 'Your task id is '.$task_id.' for : '.$data.PHP_EOL);

    echo 'Dispatch AsyncTask from client '.$fd.': id='.$task_id.PHP_EOL;
});

//处理异步任务 (此函数在task进程中执行)
$server->on('Task', function($server, $task_id, $reactor_id, $data){
    echo 'New AsyncTask to handle: id='.$task_id.PHP_EOL;

    sleep(5);//休眠5秒钟
    $strResult = rand() > 0.2 ? 'ok' : 'fail';//我们随机一个结果

    //返回执行任务的结果
    $server->finish($data.' -> '.$strResult);
});

//处理异步任务的结果,可选(忽略结果) (此回调函数在worker进程中执行)
$server->on('Finish', function($server, $task_id, $data){
    echo 'AsyncTask[id='.$task_id.'] ('.$data.') Finished at '.date('Y-m-d H:i:s').PHP_EOL;
});

//监听连接关闭事件
$server->on('Close', function($server, $fd){
    echo date('Y-m-d H:i:s').' Client closed: fd='.$fd.PHP_EOL;
});

$server->start();

调用 $serv->task() 后,程序立即返回,继续向下执行代码。onTask 回调函数 Task 进程池内被异步执行。执行完成后调用 $serv->finish() 返回结果。

如果当前待处理任务数量 大于 当前idle的线程, 系统会提示, 但是会在有空闲进程时处理. so, 我们上面的demo代码中设置了 sleep 5秒来测试多个任务的接收和处理.

运行

使用方式与TCP服务一致, 我们这里使用telnet, 也可以使用netcat.

php async_task.php

tcp服务开启后, 我们开4个telnet, 连接:

telnet 10.0.2.7 81

执行后我们可以在服务端看到客户端的连接:

客户端显示类似如下:

然后在每个连接中都发送文字add(只是演示):

因为我们只开启了一个线程, 所以这个线程也会被阻塞. 现实的使用中是要根据实际业务量来设置的.

如果把线程数该为4, 并在 onReceive 函数中给客户端输出信息中增加处理线程的id, 修改后如下:

$server = new Swoole\Server('10.0.2.7', 81);//虚拟机外部访问地址: 127.0.0.1:40181

//设置异步任务的工作进程数量
$server->set([
   'task_worker_num' => 4
]);

$server->on('Start', function($server){
    echo 'Swoole AsyncTask TCP server started at'.date('Y-m-d H:i:s').PHP_EOL;
});

//监听连接进入事件
$server->on('Connect', function($server, $fd){
    //$fd: 客户端连接的唯一标识, int
    echo date('Y-m-d H:i:s').' Client connected fd='.$fd.PHP_EOL;//在服务器命令行端显示文字
    $server->send($fd, 'Welcome, your ID is: '.$fd.PHP_EOL);
});

//收到任务, 并投递异步任务 (此回调函数在worker进程中执行)
$server->on('Receive', function($server, $fd, $reactor_id, $data){
    //$reactor_id: TCP连接所在的Reactor线程ID, int
    //$data: 收到的数据内容,可能是文本或者二进制内容
    $data = trim($data);
    echo date('Y-m-d H:i:s').' Client '.$fd.' send message: '.$data.PHP_EOL;

    //投递异步任务
    $task_id = $server->task($data);

    echo 'Dispatch AsyncTask from client '.$fd.' to reactor_id='.$reactor_id.', task_id='.$task_id.PHP_EOL.PHP_EOL;

    $server->send($fd, 'Your task reactor_id='.$reactor_id.', task_id='.$task_id.' for : '.$data.PHP_EOL);

});

//处理异步任务 (此函数在task进程中执行)
$server->on('Task', function($server, $task_id, $reactor_id, $data){
    echo 'New AsyncTask to handle at reactor_id='.$reactor_id.', task_id='.$task_id.PHP_EOL;

    sleep(5);//休眠
    $strResult = rand() > 0.2 ? 'ok' : 'fail';//我们随机一个结果

    //返回执行任务的结果
    $server->finish($data.' -> '.$strResult);
});

//处理异步任务的结果,可选(忽略结果) (此回调函数在worker进程中执行)
$server->on('Finish', function($server, $task_id, $data){
    echo 'AsyncTask[task_id='.$task_id.'] ('.$data.') Finished at '.date('Y-m-d H:i:s').PHP_EOL;
});

//监听连接关闭事件
$server->on('Close', function($server, $fd){
    echo date('Y-m-d H:i:s').' Client closed: fd='.$fd.PHP_EOL;
});

$server->start();

启动服务, 并开启5个telnet客户端, 然后随便输入一个命令, 且几乎同一时间操作

奇怪的是, (多次重启服务测试)结果显示只使用了0,1,2这三个线程 那么线程3干嘛去了呢???

参数说明

回调函数中的 reactor_idfd

服务器的onConnectonReceiveonClose回调函数中会携带reactor_idfd两个参数。

  • $reactor_id是来自于哪个reactor线程
  • $fdTCP客户端连接的标识符(TCP连接的文件描述符, file description),在Server实例中是唯一的,在多个进程内不会重复
  • fd 是一个自增数字,范围是1 ~ 1600万,fd超过1600万后会自动从1开始进行复用

1600w, 这个数字好熟悉啊, 2^24

  • $fd是复用的,当连接关闭后fd会被新进入的连接复用 (不会立即复用, 而是到达1600万以后开始寻找空闲fd复用)
  • 正在维持的TCP连接fd不会被复用

调用Server->send/Server->close函数需要传入$fd参数才能被正确的处理。如果业务中需要发送广播,需要用apcredisMySQLmemcacheSwoole\Tablefd的值保存起来。

function my_onReceive($serv, $fd, $reactor_id, $data)  {
    //向Connection发送数据
    $serv->send($fd, 'Swoole: '.$data);

    //关闭Connection
    $serv->close($fd);
}

fd为什么使用整型

$fd使用整型而不是使用对象,主要原因是Swoole是多进程的模型,在Worker进程/Task进程中随时可能要访问某一个客户端连接,如果使用对象,那就需要进行Serialize/Unserialize, 增加了额外的性能开销。$fd 如果是整数那就可以直接存储传输被使用。

在PHP层可以自行将客户端连接封装成对象。面向对象的好处是可读性更好,对连接的操作可以封装到方法中。如

$connection->send($data);
$connection->close();
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值