目录
在之前的 PHP Swoole的基本用法_浮尘笔记的博客-CSDN博客 中演示了Swoole对于TCP、UDP、HTTP、WebSocket的基本用法,本篇内容将主要演示Swoole中协程的使用。
进程、线程、协程
进程:是一个"执行中的程序”,进程的三态模型: 运行、就绪、堵塞。
线程:是进程中的一个实体,是被操作系统独立调度和分派的基本单位。线程是由操作系统控制的。
协程:是一种用户态的轻量级线程,协程的调度由用户控制。一个线程可以拥有多个协程,一个进程也可以单独拥有多个协程。
关于协程
- 协程在阻塞的时候只是阻塞了当前这个协程并不会阻塞整个的进程因为协程是在线程内部的,即使阻塞了也会让出控制权,挂起,等待当前协程的IO不阻塞在回来继续执行,也就是同步的代码完成了异步的功能。
- 协程是在单进程单线程当中实现的,可以在里面实现成千上万的协程,并且效果极高。每个协程去干不同的事!协作之间无需加锁没有抢占,串行的。
- 协程之间每秒可以进行百万千万次切换!线程之间切换需要加锁,加锁就很浪费资源,进程间切换就更浪费资源了。
- swoole提供了常驻内存、协程异步,这让PHP高性能微服务架构成为现实。
- 从 4.0 版本开始 Swoole 提供了完整的协程(Coroutine)+ 通道(Channel)特性,带来全新的 CSP 编程模型。
- Channel 可以理解为消息队列,只不过是协程间的消息队列,多个协程通过 push 和 pop 操作队列中的生产消息和消费消息,用来发送或者接收数据进行协程之间的通讯。需要注意的是 Channel 是没法跨进程的,只能一个 Swoole 进程里的协程间通讯,最典型的应用是连接池和并发调用。
- 参考:Swoole 文档
使用 Swoole 实现协程数据库连接池的代码
<?php
namespace app\service;
use Swoole\Coroutine\MySQL;
use Swoole\Coroutine\Channel;
use function Swoole\Coroutine\run;
/**
* Swoole 实现Mysql数据库协程连接池
* @package app\service
*/
class MysqlPool {
protected $size = 8;
protected $pool;
private $config;
private static $instance;
private function __construct($config) {
if (!$this->pool) {
run(function () use ($config) {
if (isset($config['size']) && $config['size']) {
$this->size = $config['size'];
}
$this->pool = new Channel($this->size);
//循环创建连接对象
for ($i = 0; $i < $this->size; $i++) {
//在协程中创建数据库
go(function () use ($config) {
$swoole_mysql = new MySQL();
$swoole_mysql->connect([
'host' => $config['host'],
'port' => $config['port'],
'user' => $config['user'],
'password' => $config['password'],
'database' => $config['dbname'],
'charset' => $config['charset'],
]);
if ($swoole_mysql->connect_errno != 0) {
throw new \RuntimeException('mysql connect error');
} else {
$this->pool->push($swoole_mysql);
}
});
}
});
}
}
public static function getInstance($config = []) {
if (!self::$instance) {
if (!$config) {
throw new \RuntimeException('config error');
}
self::$instance = new self($config);
}
return self::$instance;
}
/**
* 获取连接对象
* @return mixed
*/
public function get() {
//通过协程来实现
run(function () {
go(function () {
//先判断是否有空闲的
if ($this->pool->length() > 0) {
$this->mysql = $this->pool->pop();
if (!$this->mysql) {
throw new \RuntimeException('mysql timeout');
}
defer(function () {
$this->pool->push($this->mysql);
});
} else {
throw new \RuntimeException('pool length < 0');
}
});
});
return $this->mysql;
}
private function __clone() {
}
}
//调用
$config = [
'size' => 4,
'host' => '127.0.0.1',
'port' => 3306,
'dbname' => 'swoole',
'user' => 'root',
'password' => '123456',
'charset' => 'utf8'
];
$res = MysqlPool::getInstance($config)->get();
var_dump($res);
以上代码涉及到的单例模式分析过程可以参考:PHP设计模式之单例模式_浮尘笔记的博客-CSDN博客
项目源代码:app/service/MysqlPool.php · 浮尘/thinkphp-demo-2023 - Gitee.com
实际上,Swoole 从 v4.4.13 版本开始提供了内置协程连接池,具体可以参考:https://wiki.swoole.com/#/coroutine/conn_pool
Swoole中的五种进程
Master 进程、Reactor 线程、Worker 进程、TaskWorker 进程、Manager 进程:https://wiki.swoole.com/#/learn?id=diff-process
- Master 进程是一个多线程进程;
- Reactor 线程是在 Master 进程中创建的线程,负责维护客户端
TCP
连接、处理网络IO
、处理协议、收发数据,不执行任何 PHP 代码; - Worker 进程接受由
Reactor
线程投递的请求数据包,并执行PHP
回调函数处理数据,以多进程的方式运行; TaskWorker 进程
处理任务,并将结果数据返回,同步阻塞模式,以多进程的方式运行;- Manager 进程负责创建 / 回收
worker
/task
进程
他们之间的关系可以理解为 Reactor 就是 nginx,Worker 就是 PHP-FPM。Reactor 线程异步并行地处理网络请求,然后再转发给 Worker 进程中去处理。Reactor 和 Worker 间通过 unixSocket 进行通信。
下面用之前演示过的 Swoole执行异步任务 (Task) 的代码,修改 worker_num为2,task_worker_num为4,代码如下:
<?php
$http = new Swoole\Http\Server('0.0.0.0', 9501);
$http->set([
//设置启动的 Worker 进程数。【默认值:CPU 核数】
'worker_num' => 2,
//如果要使用 Task ,需要先设置 task_worker_num ,它代表的是开启的 Task 进程数量。
'task_worker_num' => 4,
]);
$http->on('Request', function ($request, $response) use ($http) {
echo "接收到了请求", PHP_EOL;
});
$http->on('Task', function ($serv, $task_id, $reactor_id, $data) {
$serv->finish("{$data} -> OK");
});
$http->on('Finish', function ($serv, $task_id, $data) {
echo "AsyncTask[{$task_id}] Finish: {$data}" . PHP_EOL;
});
$http->start();
启动之后,使用 ps -ef | grep TASK.php 命令查看当前启动的进程信息
可以看到master进程的ID是6068,然后使用 pstree -p 6068 命令查看进程树:
假设现在有个功能:发布完文章之后,需要发送邮件、短信、APP通知等等操作,一般都是通过消息队列异步消费处理。如果使用Swoole的话就可以通过task进程池处理,逻辑图如下:
但是需要注意,上面使用Swoole的task进程池可能会造成单台服务器压力过大,实际开发中还要根据业务场景灵活应用。
Swoole创建子进程
先看看使用PHP的pcntl函数创建进程的用法:
<?php
//PHP多进程和多线程的处理
//创建socket监听
$socketserv = stream_socket_server('tcp://0.0.0.0:8000', $errno, $errstr);
//创建5个子进程
for ($i = 0; $i < 5; $i++) {
//使用pcntl_fork()创建进程,会返回pid,如果pid==0,则表示主进程
if (pcntl_fork() == 0) {
//循环监听
while (true) {
$conn = stream_socket_accept($socketserv);
//如果监听失败,则重新去监听
if(!$conn){
continue;
}
//读取流信息,读取的大小 是9000
$request = fread($conn, 9000);
//写入响应
$response = 'hello';
fwrite($conn, $response);
//关闭流
fclose($conn);
}
//创建完所有的子进程,然后退出
exit(0);
}
}
运行 php stream_socket.php,使用ps -ef 查看进程,会看到多出了如下的5个进程:
PHP自带的pcntl,存在很多不足,如:
- 没有提供进程间通信的功能
- 不支持重定向标准输入和输出
- 只提供了fork这样原始的接口,容易使用错误
Process提供了比pcntl更强大的功能,更易用的API,使PHP在多进程编程方面更加轻松。
先来一段代码感受一下:
<?php
/**
* swoole 子进程
*/
$process = new Swoole\Process(function (Swoole\Process $pro) {
echo "输出内容"; //第二个参数如果是false,会将输出打印出来.如果是true则不会打印输出
//exec方法执行一个外部程序,注意需要使用php进程的绝对路径
//下面表示开启一个子进程,运行 php process.php
$pro->exec("/Applications/MxSrvs/bin/php/bin/php", [__DIR__ . '/HTTP.php']);
}, false);
$pid = $process->start(); //输出子进程的pid
echo $pid . PHP_EOL;
swoole_process::wait(); //回收结束运行的子进程(子进程结束必须要执行wait进行回收,否则子进程会变成僵尸进程)
swoole子进程的使用场景
例如:有6个网页的内容需要抓取,原始方案只能顺序抓取,执行很慢。假设一个页面需要1s,那么总共需要6s。但是如果开启6个子进程那么总共只需要1s。
先看看传统的顺序阻塞处理
<?php
$start_time = microtime(true);
$workers = [];
$urls = [
'https://www.baidu.com',
'https://www.sina.com.cn',
'https://www.qq.com',
'https://blog.csdn.net',
'https://www.swoole.com',
'https://www.taobao.com',
];
foreach ($urls as $url) {
echo curlData($url);
}
$end_time = microtime(true);
echo "time_cost: " . intval($end_time - $start_time) . " s" . PHP_EOL;
exit;
现在改成使用子进程的方式
for ($i = 0; $i < count($urls); $i++) {
// 子进程,总共只需要 1s
$process = new swoole_process(function (swoole_process $worker) use ($i, $urls) {
// curl
$content = curlData($urls[$i]);
//echo $content.PHP_EOL;
$worker->write($content); //把数据写入管道
}, true);
$pid = $process->start();
$workers[$pid] = $process;
}
foreach ($workers as $process) {
echo $process->read(); //从管道中读取数据
}
$end_time = microtime(true);
项目源代码:https://gitee.com/rxbook/thinkphp-demo-2023/blob/master/swoole_test1/process_curl.php