PHP多进程系列笔记(五)
前面几节都是讲解pcntl扩展实现的多进程程序。本节给大家介绍swoole扩展的swoole_process
模块。
swoole多进程
swoole_process 是swoole提供的进程管理模块,用来替代PHP的pcntl扩展。
首先,确保安装的swoole版本大于1.7.2:
$ php --ri swoole
swoole
swoole support => enabled
Version => 1.10.1
注意:swoole_process在最新的1.8.0版本已经禁止在Web环境中使用了,所以也只能支持命令行。
swoole提供的多进程扩展基本功能和pcntl提供的一样,但swoole更易简单上手,并且提供了:
- 默认基于unixsock的进程间通信;
- 支持消息队列作为进程间通信;
- 基于signalfd和eventloop处理信号,几乎没有任何额外消耗;
- 高精度微秒定时器;
- 配合swoole_event模块,创建的PHP子进程可以异步的事件驱动模式
swoole_process 模块提供的方法(Method)主要分为四部分:
- 基础方法
swoole_process::__construct
swoole_process->start
swoole_process->name
swoole_process->exec
swoole_process->close
swoole_process->exit
swoole_process::kill
swoole_process::wait
swoole_process::daemon
swoole_process::setAffinity
- 管道通信
swoole_process->write
swoole_process->read
swoole_process->setTimeout
swoole_process->setBlocking
- 消息队列通信
swoole_process->useQueue
swoole_process->statQueue
swoole_process->freeQueue
swoole_process->push
swoole_process->pop
- 信号与定时器
swoole_process::signal
swoole_process::alarm
基础应用
本例实现的是tcp server,特性:
- 多进程处理客户端连接
- 子进程退出,Master进程会重新创建一个
- 支持事件回调
- 主进程退出,子进程在干完手头活后退出
<?php
class TcpServer{
const MAX_PROCESS = 3;//最大进程数
private $pids = []; //存储子进程pid
private $socket;
private $mpid;
public function run(){
$process = new swoole_process(function(){
$this->mpid = $id = getmypid();
echo time()." Master process, pid {$id}\n";
//创建tcp server
$this->socket = stream_socket_server("tcp://0.0.0.0:9201", $errno, $errstr);
if(!$this->socket) exit("start server err: $errstr --- $errno");
for($i=0; $i<self::MAX_PROCESS;$i++){
$this->start_worker_process();
}
echo "waiting client...\n";
//Master进程等待子进程退出,必须是死循环
while(1){
foreach($this->pids as $k=>$pid){
if($pid){
$res = swoole_process::wait(false);
if ( $res ){
echo time()." Worker process $pid exit, will start new... \n";
$this->start_worker_process();
unset($this->pids[$k]);
}
}
}
sleep(1);//让出1s时间给CPU
}
}, false, false); //不启用管道通信
swoole_process::daemon(); //守护进程
$process->start();//注意:start之后的变量子进程里面是获取不到的
}
/**
* 创建worker进程,接受客户端连接
*/
private function start_worker_process(){
$process = new swoole_process(function(swoole_process $worker){
$this->acceptClient($worker);
}, false, false);
$pid = $process->start();
$this->pids[] = $pid;
}
private function acceptClient(&$worker)
{
//子进程一直等待客户端连接,不能退出
while(1){
$conn = stream_socket_accept($this->socket, -1);
if($this->onConnect) call_user_func($this->onConnect, $conn); //回调连接事件
//开始循环读取消息
$recv = ''; //实际收到消息
$buffer = ''; //缓冲消息
while(1){
$this->checkMpid($worker);
$buffer = fread($conn, 20);
//没有收到正常消息
if($buffer === false || $buffer === ''){
if($this->onClose) call_user_func($this->onClose, $conn); //回调断开连接事件
break;//结束读取消息,等待下一个客户端连接
}
$pos = strpos($buffer, "\n"); //消息结束符
if($pos === false){
$recv .= $buffer;
}else{
$recv .= trim(substr($buffer, 0, $pos+1));
if($this->onMessage) call_user_func($this->onMessage, $conn, $recv); //回调收到消息事件
//客户端强制关闭连接
if($recv == "quit"){
echo "client close conn\n";
fclose($conn);
break;
}
$recv = ''; //清空消息,准备下一次接收
}
}
}
}
//检查主进程是否存在,若不存在子进程在干完手头活后退出
public function checkMpid(&$worker){
if(!swoole_process::kill($this->mpid,0)){
$worker->exit();
// 这句提示,实际是看不到的.需要写到日志中
echo "Master process exited, I [{$worker['pid']}] also quit\n";
}
}
function __destruct() {
@fclose($this->socket);
}
}
$server = new TcpServer();
$server->onConnect = function($conn){
echo "onConnect -- accepted " . stream_socket_get_name($conn,true) . "\n";
fwrite($conn,"conn success\n");
};
$server->onMessage = function($conn,$msg){
echo "onMessage --" . $msg . "\n";
fwrite($conn,"received ".$msg."\n");
};
$server->onClose = function($conn){
echo "onClose --" . stream_socket_get_name($conn,true) . "\n";
fwrite($conn,"onClose "."\n");
};
$server->run();
运行后可以使用telnet连接:
telnet 127.0.0.1 9201
由于设置了最大三个子进程,最多只能接受3个客户端连接。
进程间通信
前面讲解的例子里,主进程和子进程直接是没有直接的数据交互的。如果主进程需要得到的来自子进程的反馈,或者子进程接受来自主进程的数据,那么就需要进程间通信了。
swoole内置了管道通信和消息队列通信。
管道通信
管道通信主要是数据传输:一个进程需要将数据发送给另外一个进程。
这个swoole封装后,使用非常简单:
<?php
$workers = [];
for ($i=0; $i<3; $i++) {
$process = new swoole_process(function(swoole_process $worker){
//子进程逻辑
$cmd = $worker->read();
ob_start();
passthru($cmd);//执行外部程序并且显示未经处理的、原始输出,会直接打印输出。
$return = ob_get_clean() ? : ' ';
$return = trim($return).". worker pid:".$worker->pid."\n";
// $worker->write($return);//写入数据到管道
echo $return;//写入数据到管道。注意:子进程里echo也是写入到管道
}, true); //第二个参数为true,启用管道通信
$pid = $process->start();
$workers[$pid] = $process;
}
foreach($workers as $pid=>$worker){
$worker->write('whoami'); //通过管道发数据到子进程。管道是单向的:发出的数据必须由另一端读取。不能读取自己发出去的
$recv = $worker->read();//同步阻塞读取管道数据
echo "recv result: $recv";
}
//回收子进程
while(count($workers)){
// echo time(). "\n";
foreach($workers as $pid=>$worker){
$ret = swoole_process::wait(false);
if($ret){
echo "worker exit: $pid\n";
unset($workers[$pid]);
}
}
}
运行:
$ php swoole_process_pipe.php
recv result: Linux
recv result: 2018年 06月 24日 星期日 16:18:01 CST
recv result: yjc
worker exit: 14519
worker exit: 14522
worker exit: 14525
注意点:
1、管道数据读取是同步阻塞的;上面的例子里如果子进程里再加一句$worker->read()
,会一直阻塞。可以使用swoole_event_add
将管道加入到事件循环中,变为异步模式。
2、子进程里的输出(例如echo)与write效果相同。
3、通过管道发数据到子进程。管道是单向的:发出的数据必须由另一端读取。不能读取自己发出去的。
这里额外讲解一下swoole_process::wait()
:
1、swoole_process::wait()
默认是阻塞的, swoole_process::wait(false)
则是非阻塞的;
2、swoole_process::wait()
阻塞模式调用一次仅能回收一个子进程,非阻塞模式调用一次不一定能当前就能回收子进程;
3、如果不加swoole_process::wait()
,主进程又是死循环,主进程退出后会变成僵尸进程。
ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
可以查询僵尸进程。
防盗版声明:本文系原创文章,发布于公众号飞鸿影的博客
(fhyblog)及博客园,转载需作者同意。
消息队列通信
消息队列与管道有些不一样:消息队列是全局的,所有进程都可以发送、读取。你可以把它看做redis list结构。
消息队列更常见的用途是主进程分配任务,子进程消费执行。
<?php
$workers = [];
for ($i=0; $i<3; $i++) {
$process = new swoole_process(function(swoole_process $worker){
//子进程逻辑
sleep(1); //防止父进程还未往消息队列中加入内容直接退出
while($cmd = $worker->pop()){
// echo "recv from master: $cmd\n";
ob_start();
passthru($cmd);//执行外部程序并且显示未经处理的、原始输出,会直接打印输出。
$return = ob_get_clean() ? : ' ';
$return = "res: ".trim($return).". worker pid: ".$worker->pid."\n";
echo $return;
// sleep(1);
}
$worker->exit(0);
}, false, false); //不创建管道
$process->useQueue(1, 2 | swoole_process::IPC_NOWAIT); //使用消息队列
$pid = $process->start();
$workers[$pid] = $process;
}
//由于所有进程是共享使用一个消息队列,所以只需向一个子进程发送消息即可
$worker = current($workers);
for ($i=0; $i<3; $i++) {
$worker->push('whoami'); //发送消息
}
//回收子进程
while(count($workers)){
foreach($workers as $pid=>$worker){
$ret = swoole_process::wait();
if($ret){
echo "worker exit: $pid\n";
unset($workers[$pid]);
}
}
}
运行结果:
$ php swoole_process_quene.php
res: yjc. worker pid: 15885
res: yjc. worker pid: 15886
res: yjc. worker pid: 15887
worker exit: 15885
worker exit: 15886
worker exit: 15887
注意点:
1、所有进程共享使用一个消息队列;
2、消息队列的读取操作是阻塞的,可以在useQueue
的时候第2个参数mode改为2 | swoole_process::IPC_NOWAIT
,则是异步的。mode仅仅设置为2
是阻塞的,示例里去掉swoole_process::IPC_NOWAIT
后读取消息的while会死循环。
3、子进程前面加了个sleep(1);
,这是为了防止父进程还未往消息队列中加入内容直接退出。
4、子进程末尾也加了sleep,这是为了防止一个进程把所有消息都消费完了,实际应用需要去掉。
信号与定时器
swoole_process::alarm
支持微秒定时器:
<?php
function ev_timer(){
static $i = 0;
echo "#{$i}\talarm\n";
$i++;
if ($i > 5) {
//清除定时器
swoole_process::alarm(-1);
//退出进程
swoole_process::kill(getmypid());
}
}
//安装信号
swoole_process::signal(SIGALRM, 'ev_timer');
//触发定时器信号:单位为微秒。如果为负数表示清除定时器
swoole_process::alarm(100 * 1000);//100ms
echo getmypid()."\n"; //该句会顺序执行,后续无需使用while循环防止进程直接退出
运行:
$ php swoole_process_alarm.php
13660
#0 alarm
#1 alarm
#2 alarm
#3 alarm
#4 alarm
#5 alarm
注:alarm不能和
Swoole\Timer
同时使用。
参考
1、Process-Swoole-Swoole文档中心
https://wiki.swoole.com/wiki/page/p-process.html
PHP多进程系列笔记(五)的更多相关文章
- PHP多进程系列笔记(一)
本系列文章将向大家讲解pcntl_*系列函数,从而更深入的理解进程相关知识. PCNTL在PHP中进程控制支持默认是关闭的.您需要使用 --enable-pcntl 配置选项重新编译PHP的 CGI或 ...
- PHP多进程系列笔记(三)
本节讲解几个多进程的实例. 多进程实例 Master-Worker结构 下面例子实现了简单的多进程管理: 支持设置最大子进程数 Master-Worker结构:Worker挂掉,Master进程会重新 ...
- PHP多进程系列笔记(二)
上一篇文章讲解了pcntl_fork和pcntl_wait两个函数的使用,本篇继续讲解PHP多进程相关新知识. 僵尸(zombie)进程 这里说下僵尸进程: 僵尸进程是指的父进程已经退出,而该进程de ...
- PHP多进程系列笔记(四)
本节主要讲解Posix常用函数和进程池的概念,也会涉及到守护进程的知识.本节难度较低. Posix常用函数 posix_kill 向指定pid进程发送信号.成功时返回 TRUE , 或者在失败时返回 ...
- C#可扩展编程之MEF学习笔记(五):MEF高级进阶
好久没有写博客了,今天抽空继续写MEF系列的文章.有园友提出这种系列的文章要做个目录,看起来方便,所以就抽空做了一个,放到每篇文章的最后. 前面四篇讲了MEF的基础知识,学完了前四篇,MEF中比较常用 ...
- 跟着鸟哥学Linux系列笔记1
跟着鸟哥学Linux系列笔记0-扫盲之概念 跟着鸟哥学Linux系列笔记0-如何解决问题 装完linux之后,接下来一步就是进行相关命令的学习了 第五章:首次登录与在线求助man page 1. X ...
- 《MFC游戏开发》笔记五 定时器和简单动画
本系列文章由七十一雾央编写,转载请注明出处. http://blog.csdn.net/u011371356/article/details/9332377 作者:七十一雾央 新浪微博:http:// ...
- node.js系列笔记之node.js初识《一》
node.js系列笔记之node.js初识<一> 一:环境说明 1.1 Linux系统CentOS 5.8 1.2 nodejs v0.10.15 1.3 nodejs源码下载地址 htt ...
- Java系列笔记(5) - 线程
我想关注这个系列博客的粉丝们都应该已经发现了,我一定是个懒虫,在这里向大家道歉了.这个系列的博客是在我工作之余写的,经常几天才写一小节,不过本着宁缺毋滥的精神,所有写的东西都是比较精炼的.这篇文章是本 ...
随机推荐
- 2.2.11同步synchronized方法无限等待与解决
同步方法容易造成死循环. package com.cky.bean; /** * Created by edison on 2017/12/8. */ public class Service { s ...
- Is Docker Good for Your Database?
https://dzone.com/articles/is-docker-good-for-your-database
- Java 判断字符串能否转化为数字的三种方法
用JAVA自带的函数 public static boolean isNumeric(String str){ for (int i = str.length();--i>=0;){ if (! ...
- java基础-day13
第01天 java面向对象 今日内容介绍 u 继承 u 抽象类 第1章 继承 1.1 继承的概述 在现实生活中,继承一般指的是子女继承父辈的财产.在程序中,继承描述的是事物之间的所属关系,通过继 ...
- Windows Phone 8.1不完全体验报告
在Build 2014中,微软倾心打造的Windows Phone 8.1终于粉墨登场,会场掌声不断.在大会结束后一周,经过漫长的等待,终于等到了开发者预览的推送,迫不及待地体验这一跨时代的移动系统. ...
- js五道经典练习题--第二道仿qq聊天框
<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title>& ...
- Maven中项目的启动
在run选项卡中,选择Run Configurations
- css中box-sizing简单说明(标准盒式模型和怪异盒式模型)
今天写程序做布局的时候,遇到关于css中盒式模型的问题,百度了下这属性的解释,脑大啊,文字太绕看不懂.怎么办,于是自己动动手写了段程序测试,嗯,不错,一看效果就恍然大明白了.这里简单说明下,也可能说得 ...
- [javascript]Three parts of javascript code snippet
<script> (function(){ /* if (navigator.userAgent.toLowerCase().indexOf("iphone") == ...
- double float的精度问题
三部曲 1: #include <iostream> #include <stdio.h> #include <string.h> using namespace ...