PHP多进程

PHP多进程

1.多开几个进程,这种方式简单实用,推荐,比如说使用shell脚本:

#!/bin/bash
 
for((i=1;i<=8;i++))
do    
    /usr/bin/php multiprocessTest.php &
done
 
wait

2.pcntl扩展

php多进程需要pcntl,posix扩展支持,可以通过 php -m 查看,而且多进程实现只能在cli模式下,虽然是个残废,不妨也了解一下, 实际上这些都是调用了Linux的系统API
举个例子:

<?php
foreach (range(1, 5) as $index) {
    $pid = pcntl_fork();
    if ($pid === -1) {
        echo "failed to fork!\n";
        exit;
    } elseif ($pid) {
        pcntl_wait($status); //父进程必须等待一个子进程退出后,再创建下一个子进程。
        echo "I am the parent, pid: $pid\n";
    } else {
        $cid = posix_getpid();
        echo "fork the {$index}th child, pid: $cid\n";
        exit; //必须
    }
}

这个例子非常简单,循环创建5个进程,在各个进程里面打印一句话,主要使用的方法就是函数 pcntl_fork,一次调用两次返回,在父进程中返回子进程pid,在子进程中返回0,出错返回-1。

执行结果如下:

fork the 1th child, pid: 7326
I am the parent, pid: 7326
fork the 2th child, pid: 7327
I am the parent, pid: 7327
fork the 3th child, pid: 7328
I am the parent, pid: 7328
fork the 4th child, pid: 7329
I am the parent, pid: 7329
fork the 5th child, pid: 7330
I am the parent, pid: 7330

先解释一下为什么会产生10条打印结果,第一条结果是子进程打印的,第二条是在父进程打印的!

第一个坑:

如果是在循环中创建子进程,那么子进程中最后要exit,防止子进程进入循环!

第二个坑:

必须等待子进程执行完任务, 有一个简单方法是使用 pcntl_wait,如果不加这个你会发现一个是执行的顺序不固定,第二个就是创建的进程会少于5个,但是加了你会发现这个完全变成并行了...上面的结果就是

然后找了找,发现下面这种写法:

<?php

$ids = [];

foreach (range(1, 5) as $index) {
    $ids[] = $pid = pcntl_fork();
    if ($pid === -1) {
        echo "failed to fork!\n";
        exit;
    } elseif ($pid) {
        echo "I am the parent, pid: $pid\n";
    } else {
        $cid = posix_getpid();
        echo "fork the {$index}th child, pid: $cid\n";
        exit;
    }
}

foreach ($ids as $i => $pid) {
    if ($pid) {
        pcntl_waitpid($pid, $status);
    }
}

结果如下:

fork the 1th child, pid: 8392
I am the parent, pid: 8392
I am the parent, pid: 8393
fork the 2th child, pid: 8393
I am the parent, pid: 8394
I am the parent, pid: 8395
I am the parent, pid: 8396
fork the 3th child, pid: 8394
fork the 4th child, pid: 8395
fork the 5th child, pid: 8396

找了一张图,大体解释了总体流程:

image

说简单其实也挺简单,几行代码就可以写出一个多进程程序,实现并行编程,但是这里其实还有不少坑,比如僵尸进程,孤儿进程, 守护进程,具体的我也不太熟悉不多讲,再看一个关于进程信号的东西,有些项目里面有时候会用到一些脚本,比如处理redis队列的脚本,通常的做法是写一个while死循环一直从redis里面取数据处理,为了防止内存泄露或者假死,一般都会定时的杀掉脚本重启脚本,但是杀的不好可能会导致数据丢失,举个例子,假如你这个脚本刚好从redis取了一条数据正在处理中,操作还未完成,你突然终止进程,那这个数据就丢失了。至于说服务器挂掉这种情况毕竟不多见,真要解决这种问题还得从队列上入手。

<?php

//ctrl+c
pcntl_signal(SIGINT, function () {
    fwrite(STDOUT, "receive signal: " . SIGINT . " do nothing ...\n");
});

//kill
pcntl_signal(SIGTERM, function () {
    fwrite(STDOUT, "receive signal: " . SIGTERM . " I will exit!\n");
    exit;
});

while (true) {
    pcntl_signal_dispatch();
    echo "do something。。。\n";
    sleep(5);
}
image

Linux进程信号分为很多种,kill -l 可以查看,PHP里面定义了43种,咱就说说常用的几种:

SIGINT 2 这个其实相对于 ctrl+c

SIGTERM 15 就是 kill 默认的参数,表示终止信号,但是你发了信号程序不一定响应

SIGKILL 9 就是 kill -9, 表示立马终止,这个信号在PHP里面是无法注册的,所以一定能成功

看明白了这个就可以读懂上面的例子了,其中 pcntl_signal 是注册信号处理handler,第一个参数是你需要注册的信号,第二个是处理操作,可以是匿名函数或者一个函数名,可以注册多个信号。pcntl_signal_dispatch 调用每个等待信号通过pcntl_signal() 安装的处理器。早期PHP还有一种写法是使用 ticks,性能非常差,php5.3之后建议都使用 pcntl_signal_dispatch。

说明一下:pcntl_signal()函数仅仅是注册信号和它的处理方法,真正接收到信号并调用其处理方法的是pcntl_signal_dispatch()函数必须在循环里调用,为了检测是否有新的信号等待dispatching。

上面的例子执行结果就是当你使用 ctrl+c 的话是无法终止程序的,只有使用 kill pid 这种形式才可以,但是并不是立马就退出,它是代码执行到循环顶部 pcntl_signal_dispatch 地方的时候才会退出,这就保证了你使用kill杀掉进程的时候并不会丢失数据,说好听点这也算是平滑重启吧!

由于进程的系统开销比较大,一般不太适合拿来做大规模并发程序,拿来写个3-5个进程的后台脚本倒是有点用,下面就是我写的一个用来爬取xhprof的数据的脚本,使用了3个进程同时爬取实战,路径,免费课的日志然后做统计根据出现次数排序!

<?php
define("TOTAL_PAGE", 100);   //总共多少页
define("MS", 2000);          //毫秒
define("DAY", 3);            //几天内
define("SAVE_DIR", "/home/jwang");   //保存目录

$servers = [
    'mkw' => '10.100.133.99',
    'sz'  => '10.100.135.23',
    'lj'  => '10.100.17.13',
];

$ids = [];

foreach ($servers as $key => $server) {
    $ids[] = $pid = pcntl_fork();
    if ($pid === -1) {
        echo "failed to fork!\n";
        exit;
    } elseif ($pid) {
    } else {
        download($server, $key);
    }
}

foreach ($ids as $i => $pid) {
    if ($pid) {
        pcntl_waitpid($pid, $status);
    }
}

function download($server, $fileName)
{
    $saveDir = SAVE_DIR;
    if (!is_dir(SAVE_DIR)) {
        $saveDir = __DIR__;
    }

    $file = $saveDir . "/xhprof_{$fileName}_tmp.txt";
    $fp   = fopen($file, 'w+');
    foreach (range(1, TOTAL_PAGE) as $page) {
        print_r("### " . date('Y-m-d H:i:s') . ": 正在爬取 $server -> $fileName 第 $page 页...\n");
        try {
            $html = file_get_contents("http://{$server}/xhprof/index.php?page={$page}&ms=" . MS . "&day=" . DAY);
        } catch (Exception $exception) {
            var_dump("网络请求失败!\n");
            exit;
        }
        if (!$html) {
            var_dump("网络请求失败!\n");
            exit;
        }

        preg_match_all("/<a .*>(.*)<\\/a>/", $html, $matches);
        if (isset($matches[1])) {
            if (count($matches[1]) <= 3) {
                break;
            }
            foreach ($matches[1] as $match) {
                fwrite($fp, $match . "\n");
            }
        }
    }
    fclose($fp);
    print_r("### " . date('Y-m-d H:i:s') . ": 爬取完成,开始处理数据...\n");
    print_r("---------------------------------------------------------- \n");
    $fp = file($file);
    if (!$fp) {
        var_dump("文件读取失败!\n");
    }

    foreach ($fp as $key => $item) {
        $item = rtrim(parse_url(trim($item))['path'], "/");
        if (substr($item, 0, 1) != '/') {
            unset($fp[$key]);
            continue;
        }
        $fp[$key] = preg_replace("/\/\d+/", "/*", $item);
    }

    $res = array_count_values($fp);

    uasort($res, function ($a, $b) {
        return $a < $b;
    });
    $saveFile = fopen($saveDir . "/xhprof_{$fileName}.txt", 'w+');

    foreach ($res as $key => $value) {
        $key = trim($key);
        $str = sprintf("%-50s ===============> %s 次\n", $key, $value);
        fwrite($saveFile, $str);
    }

    fclose($saveFile);
    unlink($file);
    exit;
}

最后还忘记说了一个坑,在子进程里面使用mysql 或者 redis 这类程序有个bug,假如你使用的是单例模式的话,这个连接被多个子进程使用就会出问题,所以如果要使用,必须在各个子进程内部新建一个连接!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容