3、PHP Socket 网络编程 - 挺进 PHP 多进程

作者: 温新

图书: 【PHP Socket 网络编程】

阅读: 658

时间: 2025-03-20 05:50:56

hi,我是温新,一名 PHPer

上篇文章学习了如何使用 pcntl_fork 来创建子进程,本篇文章承接上文,来看看多进程是怎么创建的及有什么“问题”出现?

多进程是你想象中的结果吗

下面的代码,是一个创建多进程的代码。

<?php
// 3-1-pcntl.php

for ($i = 1; $i <= 3; $i++) {
    $pid = pcntl_fork();
    if ($pid == 0) {
        echo '我是儿子' . PHP_EOL;
    }
}

先来分析一下这段代码,乍一看,这还用分析吗?循环了 3 次,那肯定是输出 3 次 我是儿子!,你斩钉截铁地的说,心里也是这样想的,小样,就样?

如果你真的是这样想的,那么,小伙子,还是太年轻了。应当静下心来,重新自我思索一下,它究竟输出了几次 我是儿子

...

此处是用于思考的时间

...

如果,左思右想都是 3 次,那么,不妨执行下程序来看看结果吧

$ php 3-1-pcntl.php 
我是儿子
我是儿子
我是儿子
我是儿子
我是儿子
我是儿子
我是儿子

看着输出的结果明显大于 3 次,此刻,我的大脑已经处于蒙圈状态了。天呐,数一数,7 次,TA 竟然欺骗了我的感情,7 为何来?又当如何才能输出预期中的 3 次?

预期中的多进程结果

左思右想都想不明白为什么会输出 7 次,原因为何?先不着急,从简单处着手,先让它输出预期中的 3 次,之后我们再来分析为什么会输出 7 次。

<?php
// 3-2-pcntl.php

for ($i = 1; $i <= 3; $i++) {
    $pid = pcntl_fork();
    if ($pid == 0) {
        echo '我是儿子' . PHP_EOL;
        exit;
    }
}

看看代码,不同之处在于,我们多加了一个 exit;来看看输出结果

$ php 3-2-pcntl.php 
我是儿子
我是儿子
我是儿子

加了一个 exit,竟然变成了预期中的结果,此刻,疯狂的抓了一下脑袋,这是为毛,我的脑袋要炸裂了。

此时,一定要冷静呀,千万不能前功尽弃,还没有开始就已经结束了。

下面我们就来分析一下原因。

多进程结果不符合预期的原因

何为 7 次

出现不符合预期的结果的原因,终究还是得回到 pcntl_fork 身来,它会为子进程创建一个副本,并在调用的时候,父进程和子进程都会继续执行后面的代码。

有了这个理解,我们再来分析原因:

  • 第一次循环
    • 执行 pcntl_fork 函数时,主进程(A)会创建一个子进程 (B)
    • 在父进程(A)中,pcntl_fork 返回子进程 B 的进程 ID,此时,主进程不会输出 "我是儿子"
    • 在子进程(B)中,pcntl_fork 返回 0,因此,输出 "我是儿子"
  • 第二次循环(注意,此时已经有 A和B 两个进程了)
    • 进程 A 再执行 pcntl_fork 函数,创建一个子进程 C
    • 由于进程 B 是独立的,因此它会创建一个子进程 D,且 B 和 D 都会输出 "我是儿子"
  • 第三次循环(此时有 4 个进程了)
    • 同理,进程 A 和 C 分别创建自己的子进程 E 和 F,B 和 D 分别创建自己的子进程 G 和 H
    • 因此,进程 E、F、G、H 都会输出 “我是儿子”

此时输出结果为 1 + 2 + 4 = 7,也就输出了 7 次我是儿子。

题外话,对于 N 次循环,创建的子进程数为 1+2+4...,为 2 的 N 次方 - 1

何为预期中的 3 次

有了上面的基础,再来理解为什么使用了 exit 就能输出预期中的 3 次了。

使用 exit 中断了父进程的子进程继续生成子进程。如此,也就输出了预期中的 3 次。

为了便于理解,我们再次改造代码,输出循环此处与主进程 ID 和子进程 ID。

<?php
// 3-3-pcntl.php

for ($i = 1; $i <= 3; $i++) {
    $pid = pcntl_fork();
    if ($pid == 0) {
        echo '第 ' . $i . ' 次循环 - 主进程 ID:' . posix_getppid() . ' fork 子进程:' . posix_getpid() . PHP_EOL;
    }
}

输出结果

$ php 3-3-pcntl.php 
第 1 次循环 - 主进程 ID:67095 fork 子进程:67096
第 2 次循环 - 主进程 ID:67095 fork 子进程:67097
第 3 次循环 - 主进程 ID:67095 fork 子进程:67098
第 2 次循环 - 主进程 ID:67096 fork 子进程:67099
第 3 次循环 - 主进程 ID:67097 fork 子进程:67100
第 3 次循环 - 主进程 ID:67096 fork 子进程:67101
第 3 次循环 - 主进程 ID:67099 fork 子进程:67102

大家可以结合这个循环输出的进程号与 何为 7 次 一起理解。

多进程数据处理

多进程向同一个文件中写入数据可能存在数据被覆盖的情况,我们针对这个情况来做一个测试。

开 1000 个进程向同一个文件中写入数据。(PS:1000 个进程不是开玩笑的,搞不好电脑就得的重启~~)

<?php
// 3-4-pcntl.php

for ($i = 1; $i <= 1000; $i++) {
    $pid = pcntl_fork();
    if ($pid == 0) {
        file_put_contents('./3-4-pcntl.log', posix_getpid() . '--'.  $i . PHP_EOL, FILE_APPEND);
        exit;
    }
}

不管你信不信,反正我是信了,若不信你可以把 exit 注释掉,看看你的电脑会发生啥。下面我们看看执行结果

$ php 3-4-pcntl.php

$ tail -n 2 3-4-pcntl.log 
15271--999
15272--1000

结果是 1000 行稳当的躺在文件中,并没有发生覆盖的情况,这是为啥?

当使用 file_put_content 时并启用 FILE_APPEND 标记,那么,无论多少进程向同一个文件中写入数据,都不会出现内容覆盖的情况。

至此,本篇文章也就结束了。

结束之前,大家一起来乐一下,下图就是我没有加 exit 导致电脑卡死,不得不重启的血泪截图!

我创建了一个 1000 的 N 次方 - 1 个进程,从截图结果来看,出现了 N 多 NULL,这是为何?你知道原因吗。欢迎评论留言。

请登录后再评论