网页的本质就是超级文本标记语言,通过结合使用其他的Web技术(如:脚本语言、公共网关接口、组件等),可以创造出功能强大的网页。因而,超级文本标记语言是万维网(Web)编程的基础,也就是说万维网是建立在超文本基础之上的。超级文本标记语言之所以称为超文本标记语言,是因为文本中包含了所谓“超级链接”点。
本篇文章给大家带来的内容是关于php协成实现的详解(附代码),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。
实现 PHP 协程需要了解的基本内容。
多进程/线程
最早的服务器端程序都是通过多进程、多线程来解决并发IO的问题。进程模型出现的最早,从Unix
系统诞生就开始有了进程的概念。最早的服务器端程序一般都是 Accept
一个客户端连接就创建一个进程,然后子进程进入循环同步阻塞地与客户端连接进行交互,收发处理数据。
多线程模式出现要晚一些,线程与进程相比更轻量,而且线程之间共享内存堆栈,所以不同的线程之间交互非常容易实现。比如实现一个聊天室,客户端连接之间可以交互,聊天室中的玩家可以任意的其他人发消息。用多线程模式实现非常简单,线程中可以直接向某一个客户端连接发送数据。而多进程模式就要用到管道、消息队列、共享内存等等统称进程间通信(IPC)复杂的技术才能实现。
最简单的多进程服务端模型
$serv = stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr)
or die("Create server failed");
while(1) {
$conn = stream_socket_accept($serv);
if (pcntl_fork() == 0) {
$request = fread($conn);
// do something
// $response = "hello world";
fwrite($response);
fclose($conn);
exit(0);
}
}
多进程/线程模型的流程是:
创建一个 socket,绑定服务器端口(bind),监听端口(listen),在 PHP 中用 stream_socket_server 一个函数就能完成上面 3 个步骤,当然也可以使用更底层的sockets 扩展分别实现。
进入 while 循环,阻塞在 accept 操作上,等待客户端连接进入。此时程序会进入睡眠状态,直到有新的客户端发起 connect 到服务器,操作系统会唤醒此进程。accept 函数返回客户端连接的 socket 主进程在多进程模型下通过 fork(php: pcntl_fork)创建子进程,多线程模型下使用 pthread_create(php: new Thread)创建子线程。
下文如无特殊声明将使用进程同时表示进程/线程。
子进程创建成功后进入 while 循环,阻塞在 recv(php:fread)调用上,等待客户端向服务器发送数据。收到数据后服务器程序进行处理然后使用 send(php: fwrite)向客户端发送响应。长连接的服务会持续与客户端交互,而短连接服务一般收到响应就会 close。
当客户端连接关闭时,子进程退出并销毁所有资源,主进程会回收掉此子进程。
这种模式最大的问题是,进程创建和销毁的开销很大。所以上面的模式没办法应用于非常繁忙的服务器程序。对应的改进版解决了此问题,这就是经典的 Leader-Follower 模型。
$serv = stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr)
or die("Create server failed");
for($i = 0; $i < 32; $i++) {
if (pcntl_fork() == 0)
{
while(1) {
$conn = stream_socket_accept($serv);
if ($conn == false) continue;
// do something
$request = fread($conn);
// $response = "hello world";
fwrite($response);
fclose($conn);
}
exit(0);
}
}它的特点是程序启动后就会创建 N 个进程。每个子进程进入 Accept,等待新的连接进入。当客户端连接到服务器时,其中一个子进程会被唤醒,开始处理客户端请求,并且不再接受新的 TCP 连接。当此连接关闭时,子进程会释放,重新进入 Accept,参与处理新的连接。
这个模型的优势是完全可以复用进程,没有额外消耗,性能非常好。很多常见的服务器程序都是基于此模型的,比如 Apache、PHP-FPM。
多进程模型也有一些缺点。
这种模型严重依赖进程的数量解决并发问题,一个客户端连接就需要占用一个进程,工作进程的数量有多少,并发处理能力就有多少。操作系统可以创建的进程数量是有限的。
启动大量进程会带来额外的进程调度消耗。数百个进程时可能进程上下文切换调度消耗占 CPU 不到 1% 可以忽略不计,如果启动数千甚至数万个进程,消耗就会直线上升。调度消耗可能占到 CPU 的百分之几十甚至 100%。
并行和并发
谈到多进程以及类似同时执行多个任务的模型,就不得不先谈谈并行和并发。
并发(Concurrency)
是指能处理多个同时活动的能力,并发事件之间不一定要同一时刻发生。
并行(Parallesim)
是指同时刻发生的两个并发事件,具有并发的含义,但并发不一定并行。
区别
『并发』指的是程序的结构,『并行』指的是程序运行时的状态
『并行』一定是并发的,『并行』是『并发』设计的一种
单线程永远无法达到『并行』状态
正确的并发设计的标准是:
使多个操作可以在重叠的时间段内进行。
two tasks can start, run, and complete in overlapping time periods
迭代器 & 生成器
在了解 PHP 协程前,还有 迭代器 和 生成器 这两个概念需要先认识一下。
迭代器
PHP5 开始内置了 Iterator 即迭代器接口,所以如果你定义了一个类,并实现了Iterator 接口,那么你的这个类对象就是 ZEND_ITER_OBJECT 即可迭代的,否则就是 ZEND_ITER_PLAIN_OBJECT。
对于 ZEND_ITER_PLAIN_OBJECT 的类,foreach 会获取该对象的默认属性数组,然后对该数组进行迭代。
而对于 ZEND_ITER_OBJECT 的类对象,则会通过调用对象实现的 Iterator 接口相关函数来进行迭代。
任何实现了 Iterator 接口的类都是可迭代的,即都可以用 foreach 语句来遍历。
Iterator 接口
interface Iterator extends Traversable
{
// 获取当前内部标量指向的元素的数据
public mixed current()
// 获取当前标量
public scalar key()
// 移动到下一个标量
public void next()
// 重置标量
public void rewind()
// 检查当前标量是否有效
public boolean valid()
}常规实现 range 函数
PHP 自带的 range 函数原型:
range — 根据范围创建数组,包含指定的元素
array range (mixed $start , mixed $end [, number $step = 1 ])
建立一个包含指定范围单元的数组。
在不使用迭代器的情况要实现一个和 PHP 自带的 range 函数类似的功能,可能会这么写:
function range ($start, $end, $step = 1){
$ret = [];
for ($i = $start; $i <= $end; $i += $step) {
$ret[] = $i;
}
return $ret;
}需要将生成的所有元素放在内存数组中,如果需要生成一个非常大的集合,则会占用巨大的内存。
迭代器实现 xrange 函数
来看看迭代实现的 range,我们叫做 xrange,他实现了 Iterator 接口必须的 5 个方法:
class Xrange implements Iterator
{
protected $start;
protected $limit;
protected $step;
protected $current;
public function __construct($start, $limit, $step = 1){
$this->start = $start;
$this->limit = $limit;
$this->step = $step;
}
public function rewind(){
$this->current = $this->start;
}
public function next(){
$this->current += $this->step;
}
public function current(){
return $this->current;
}
public function key(){
return $this->current + 1;
}
public function valid(){
return $this->current <= $this->limit;
}
}使用时代码如下:
foreach (new Xrange(0, 9) as $key => $val)
{
echo $key, ' ', $val, "\n";
}
输出:
0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9看上去功能和 range() 函数所做的一致,不同点在于迭代的是一个 对象(Object) 而不是数组:
var_dump(new Xrange(0, 9));输出:
object(Xrange)#1 (4) {
["start":protected]=>
int(0)
["limit":protected]=>
int(9)
["step":protected]=>
int(1)
["current":protected]=>NULL
}另外,内存的占用情况也完全不同:
// range
$startMemory = memory_get_usage();
$arr = range(0, 500000);
echo 'range(): ', memory_get_usage() - $startMemory, " bytes\n";
unset($arr);
// xrange
$startMemory = memory_get_usage();
$arr = new Xrange(0, 500000);
echo 'xrange(): ', memory_get_usage() - $startMemory, " bytes\n";输出:
xrange(): 624 bytes
range(): 72194784 bytesrange() 函数在执行后占用了 50W 个元素内存空间,而 xrange 对象在整个迭代过程中只占用一个对象的内存。
Yii2 Query
在喜闻乐见的各种 PHP 框架里有不少生成器的实例,比如 Yii2 中用来构建 SQL 语句的 \yii\db\Query 类:
$query = (new \yii\db\Query)->from('user');
// yii\db\BatchQueryResult
foreach ($query->batch() as $users) {
// 每次循环得到多条 user 记录
}来看一下 batch() 做了什么:
/**
* Starts a batch query.
*
* A batch query supports fetching data in batches, which can keep the memory usage under a limit.
* This method will return a [[BatchQueryResult]] object which implements the [[\Iterator]] interface
* and can be traversed to retrieve the data in batches.
*
* For example,
*
*
* $query = (new Query)->from('user');
* foreach ($query->batch() as $rows) {
* // $rows is an array of 10 or fewer rows from user table
* }
*
*
* @param integer $batchSize the number of records to be fetched in each batch.
* @param Connection $db the database connection. If not set, the "db" application component will be used.
* @return BatchQueryResult the batch query result. It implements the [[\Iterator]] interface
* and can be traversed to retrieve the data in batches.
*/
public function batch($batchSize = 100, $db = null){
return Yii::createObject([
'class' => BatchQueryResult::className(),
'query' => $this,
'batchSize' => $batchSize,
'db' => $db,
'each' => false,
]);
}实际上返回了一个 BatchQueryResult 类,类的源码实现了 Iterator 接口 5 个关键方法:
class BatchQueryResult extends Object implements \Iterator
{
public $db;
public $query;
public $batchSize = 100;
public $each = false;
private $_dataReader;
private $_batch;
private $_value;
private $_key;
/**
* Destructor.
*/
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}
/**
* Resets the batch query.
* This method will clean up the existing batch query so that a new batch query can be performed.
*/
public function reset(){
if ($this->_dataReader !== null)
{
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}
/*
*
* Resets the iterator to the initial state.
* This method is required by the interface [[\Iterator]].
*/
public function rewind()
{
$this->reset();
$this->next();
}
/**
* Moves the internal pointer to the next dataset.
* This method is required by the interface [[\Iterator]].
*/
public function next()
{
if ($this->_batch === null 关键词:php协成完成的详细说明(附代码)