前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >和老李一起手撕山寨Workerman(二)

和老李一起手撕山寨Workerman(二)

作者头像
老李秀
发布2019-11-27 17:43:42
8890
发布2019-11-27 17:43:42
举报

各位巨佬、大佬、腿子们,大家好,我是老李。

我感到一阵阵地无力眩晕,确切说我的脑子里空荡荡的完全不知道要写什么,这第二篇到底该这么开张,难道我连江郎还没到就已然要才尽了吗,当我脑海里飘过这个想法后突然感到胸口一阵压抑呼吸不过来,自己奋力地想站起来却又站不起来,似乎有一双无形的大手将我狠狠地按在床上动弹不得,第二篇到底怎么开篇咋写,难道我是真的不行了吗?

就在这个时候,突然一双强有力的胳膊突然从身后抱住了我,毛茸茸的胸脯噌的我后背直痒痒,然后TA用力晃了我一下用急躁的口音对我说:

哎?老赵什么时候回来了,这狗日的昨天还在济南填坑呢,怎么今天突然冒在我床上了?...

我努力地想挥动胳膊试图保护住下边,但是就是像鬼压床那样,胳膊似乎就是不听使唤,我急的感觉都要爆炸了,突然电话铃声一顿聒噪:公司的一级报警电话就从来没有如此让我觉得亲切过...

然后我就醒了。

辣么既然梦已醒来,那么该要步入正规话题了。今天我们就正式动工破土山寨Workerman了,按照上一章拆解的模块顺序,我复制粘贴一下:

  • 进程模型
  • IO模块
  • Event-Loop模块
  • 协议模块

从这行开始,你要暂时离开一下PHP-FPM SAPI,你得准备好PHP7和PHP-CLI,然后你还需要一个真正的Linux环境(WSL和WSL2不算,真有问题的,不是我黑TA不公正对待TA),发个图你们感受下:

今天我们就从进程这里开始,然后我马上用一小段话简单概括一下进程的一些小要点:进程是操作系统进行CPU调度的最小单元,在世界上最好的语言中实现进程相关操作的是一大坨以pcntl_*为前缀的函数族(pcntl应该是process control的简称),一般说来多个进程可以加速任务完成速度,但是CPU在同一个时刻只能执行一个进程,操作系统通过调度算法在多个进程之间快速轮转CPU占用时间,弄的同一个CPU核心看起来好像同一时间真的可以支持多个进程似的然而实际上却并不是都是假象PS唬人的幻觉;其次是多个进程之间的数据是隔离的,子进程会继承走父进程的数据空间、堆、栈等信息,总之就是父子之间的正文段是共享的,但是存储空间是隔离的;虽然上一句我说了子进程会继承父进程的堆、栈等数据副本,但实际上刚fork的时候也并不是这样的,这里用到了一种叫做COW(Copy On Write)写时复制的“ 高端技术 ”,简单说如果子进程不需要修改这些信息,那么就直接与父进程共享,如果一旦子进程或父进程修改了某些信息,那么才会真正的COPY一份这个修改区域的内存数据;父进程在fork完毕后是先接着执行父进程,还是先执行子进程,这个先后顺序靠的是爱、靠的是信仰和三根香...

我不能再多说了,因为我自己刚才差点儿就吐了...再说下去估计你们可能就要点左上角的关闭了。我觉得还是需要可供CV的demo是王道...

让我们荡起双...先从pcntl_fork()开始说起,毫无疑问pcntl_fork()一定是fork()的包装,TA的作用就是搞出来一个子进程。这个函数有点儿意思,TA会返回两次,你乍一听有点困惑[ 函数怎么能返回两次呢? ]后来你灵机一动[ 嗷,是不是父进程里返回一次,子进程里返回一次 ],我在这里透过屏幕欣慰地微笑着点头...

pcntl_fork()的返回是进程号PID,PID都是大于0的,如果返回结果小于0,那就表示是出错了。在子进程里,返回的PID为0;在父进程里,返回的PID则为子进程的PID。为什么子进程里返回的PID为0?这里有个原因就是在子进程里可以通过posix_getpid()获取自己的进程号并且通过posix_getppid()来获取父进程的PID,但是在父进程可能会fork出很多个子进程,所以父进程没有办法获取某一个确切子进程的PID~

代码语言:javascript
复制
<?php
$i_pid = pcntl_fork();
// 子进程...
if ( 0 == $i_pid ) { 
  echo "I am in child process".PHP_EOL; 
}
// 父进程
else if ( $i_pid > 0 ) { 
  echo "I am in father process".PHP_EOL; 
}
else {
  throw new Exception( "Exception:pcntl_fork err" );  
}

上面代码反正能运行,只不过上面代码比较沙雕,因为说明不了什么问题,xue微修改后你们再感受下:

代码语言:javascript
复制
<?php
$s_slogan = "Hello, I'm from ";
$i_pid    = pcntl_fork();
// 子进程...
if ( 0 == $i_pid ) {
  $s_slogan .= "child process";
  echo $s_slogan." | 子进程PID:".posix_getpid()." | 父进程PID:".posix_getppid().PHP_EOL;
}
// 父进程
else if ( $i_pid > 0 ) {
  $s_slogan .= "father process";
  echo $s_slogan." | 子进程PID:".$i_pid." | 当前进程PID:".posix_getpid().PHP_EOL;
}
else {
  throw new Exception( "Exception:pcntl_fork err" ); 
}

这坨代码运行结果如下图,这坨代码可以说明两个问题:

  • 两个进程中的数据是彼此隔离不相同的
  • pcntl_fork()的返回值和父进程子进程是怎么回事

前面我说了,fork之后的代码将会分别由父进程和子进程继续执行。这句话乍一听好理解,但是我写一坨demo你们感受下:

代码语言:javascript
复制
<?php
for ( $i = 1; $i <= 3; $i++ ) {
  $i_pid = pcntl_fork();
  if ( 0 == $i_pid ) {
    echo "@子进程".PHP_EOL;
  }
}

你们猜一下上面这个代码会显示几次【@子进程】?卖个关子,你们自己去运行一下去。

如果运行结果和你们预想的结果不一样,那一定是你们想错了

我先不说为什么,咱先把上面代码xue微修改一下:

代码语言:javascript
复制
<?php
for ( $i = 1; $i <= 3; $i++ ) {
  $i_pid = pcntl_fork();
  if ( 0 == $i_pid ) {
    echo "@子进程".PHP_EOL;
    exit;
  }
}

你们复制粘贴走跑一下,感受一下结果...然后再仔细思考下,好吧?

... ...

... ...

几分钟过去了,上面那个for循环里加pcntl_fork()的问题想明白了吗?没想明白我替你下个结论:如果你想得到for循环中计数一样的子进程数,记得要在子进程里使用exit()函数,如果没有exit()函数其实这整个过程就和细胞的有丝分裂极为相似了~高中生物中的有丝分裂你们自己脑补一下吧...

好了,我假设你们整明白上面的问题了,现在我们来玩个好玩的游戏:我在pcntl_fork()执行之前先与Redis建立一个连接,然后再开3个子进程,用netstat -ant查看Redis连接数,你们猜有多少个Redis连接?

代码语言:javascript
复制
<?php
$o_redis = new Redis();
$o_redis->connect( '127.0.0.1', 6379 );
// 使用for循环搞出3个子进程来
for ( $i = 1; $i <= 3; $i++ ) {
  $i_pid = pcntl_fork();
  if ( 0 == $i_pid ) {
    // 使用while保证三个子进程不会退出...
    while( true ) {
      sleep( 1 );
    }
  }
}
// 使用while保证主进程不会退出...
while( true ) { 
  sleep( 1 );
}

我贴下截图你们感受一下,我相信你们应该能看明白这个图是啥意思...

也就是说父进程和三个子进程一共四个进程,实际上共享了一个Redis连接,而且这个Redis连接是一个实打实的长链接,这个和我们平时在PHP-FPM里用的Redis connect方法还是xue微不一样的,这种长链接避免了与Redis服务器的频繁连接(说百了就是没有三次握手和四次挥手),这种看起来似乎并不起眼的零星性能差距,将会在并发越高的时候越能甩开PHP-FPM短连接一条街...

再回到这个问题当前是四个进程共享了同一个Redis连接,这种用法会有问题么?考虑到Redis是一个单进程单线程的服务器,所有飞过去的命令本质上都是按照顺序一个一个执行的,所以似乎听起来好像没问题...我们把上面代码改一小下下,你们用心去感受一下:

代码语言:javascript
复制
<?php
$o_redis = new Redis();
$o_redis->connect( '127.0.0.1', 6379 );
// 使用for循环搞出3个子进程来
for ( $i = 1; $i <= 4; $i++ ) {
  $i_pid = pcntl_fork();
  if ( 0 == $i_pid ) {
    $b_ret = $o_redis->sismember( "uid", $i );
    echo $i.':'.json_encode( $b_ret ).PHP_EOL;
    // 使用while保证三个子进程不会退出...
    while( true ) {
      sleep( 1 );
    }
  }
}
// 使用while保证主进程不会退出...
while( true ) { 
  sleep( 1 );
}

同时,我在Redis里整了一个set集合,名称叫做uid,值为[ 1, 2, 3, 5, 6 ],注意没有4!运行结果真的是... ...

来来来,我连续运行了好多次,你们感受下:

很明显这种用法暴露了两个问题:

  • 数字4是确实不会存在uid集合中的,但是有两次结果都是true
  • 理论上应该有四条bool结果,结果有时候却只返回三条bool结果

多个进程复用同一个Redis连接,Redis的返回结果永远无法判断会被哪个进程给处理掉了,所以正确的用法应该是给每一个进程分别创建一个Redis连接,代码修改成下面这样就可以了:

代码语言:javascript
复制
<?php
$o_redis = new Redis();
// 使用for循环搞出3个子进程来
for ( $i = 1; $i <= 4; $i++ ) {
  $i_pid = pcntl_fork();
 if ( 0 == $i_pid ) {
    $o_redis->connect( '127.0.0.1', 6379 );
    $b_ret = $o_redis->sismember( "uid", $i );
    echo $i.':'.json_encode( $b_ret ).PHP_EOL;
    // 使用while保证三个子进程不会退出...
    while( true ) {
      sleep( 1 );
    }
  }
}
// 使用while保证主进程不会退出...
while( true ) {
  sleep( 1 );
}

这坨代码你们自己去测试执行吧,我就不贴运行结果了,不过我贴一张Redis连接数量的图:

四个子进程,四个与Redis的连接。

所以,如果你要用MySQL,道理也是一样的,同样也会是长链接,同样也是多进程不要共享同一个MySQL连接。

你们以为到这里就算完了吗?不,并没有,我还有一个问题,那就是file_put_contents()...我写了下面一坨代码,你们感受一下并猜测一下使用多个进程利用file_put_contents()函数向同一个文件里写数据,会出问题吗?

代码语言:javascript
复制
<?php
// 使用for循环搞出3个子进程来
for ( $i = 1; $i <= 100; $i++ ) {
  $i_pid = pcntl_fork();
  if ( 0 == $i_pid ) {
    file_put_contents( "./Core.log", $i.PHP_EOL, FILE_APPEND );
    // 使用while保证三个子进程不会退出...
    while( true ) {
      sleep( 1 );
    }
  }
}
// 使用while保证主进程不会退出...
while( true ) {
  sleep( 1 );
}

100个子进程向同一个文件里没有先后顺序地写数字,那么最终结果会写入100个数字吗?会产生进程数据覆盖的情况吗?

这个有兴趣的同学自己研究一下,没有兴趣的同学直接看推送的另外一篇文章...

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能API社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档