企业级 PHP 高并发解决方案 0x05 动态语言并发处理

架构3326 字

什么是进程、线程、协程

进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

进程的三态模型

进程的三态模型:多道程序系统中,进程在处理器上交替运行,状态不断地发生变化

运行:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目,对于单机处理系统,处于运行状态的进程只有一个。没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。

就绪:当一个程序获得了除处理机意外的一切所需资源,一旦得到处理机即可运行,则称此进程出于就绪状态。就绪进程可以按多个优先级来划分队列。例如,当一个进程处于时间片用完而进入就绪状态时,排入低优先级队列;当前进程由 I/O 操作完成而进入就绪状态时,排入高优先队列。

阻塞:也称之为等待或者睡眠状态,一个进程正在等待某一事件发生(例如请求 I/O而等待 I/O 完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程出于阻塞状态。

进程的五态模型

进程的五态模型:对于一个实际的系统,进程的状态及其转换更为复杂分为新建态、活跃就绪/静止就绪、运行、活跃阻塞/静止阻塞、终止态,可见下图。

新建态:对应于进程刚刚被创建时没有被提交的状态,并等待系统完成创建进程的所有必要信息。

活跃就绪:是指进程在主内存并且可被调度的状态。

静止就绪(挂起就绪):是指进程被对换到辅存时的就绪状态,是不能被直接调度的状态,只有当主存中没有活跃就绪态进程,或者是挂起就绪态进程具有更高的优先级,系统将被挂起就绪态进程调回主存并转化为活跃就绪。

活跃阻塞:是指进程已在主存,一旦等待事件产生便进入活跃就绪状态

静止阻塞:进程对换到辅存时的阻塞状态,一旦等地啊的事件产生便进入静止就绪状态。

终止态:进程已经结束运行,回收除进程控制块之外的其他资源,并让其他进程从进程控制块中有关信息。

线程

由于我们用户的并发请求,为每一个请求都创建一个进程显然是行不通的,从系统资源开销方面或是响应用户请求的效率方面来看。因此系统中线程概念便被引进了。

线程,有时被称之为轻量级的进程(Lightweight Process, LWP),是程序执行流的最小单元。

线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属的一个进程的其他线程共享进程所拥有的全部资源。

一个线程可以创建和撤销另一个线程,统一进程的多个线程之间可以并发执行。

线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。

在单个程序中同事运行多个线程完成不同的工作,称之为多线程。

每一个程序都只要有一个线程,若程序只有一个线程,那就是程序本身。

线程的状态:就绪阻塞运行

就绪状态:线程具备运行的所有条件,逻辑上可以运行,在等待处理机。

阻塞状态:线程在等待一个事件(如某个信号量),逻辑上不可执行。

运行状态:线程占有处理机正在运行。

协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程已拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈基本没有内核切换开销,可以不加锁访问全局变量,所以上下文的切换非常快。

如果想要深入的了解协程的实现,可以读鸟哥有关于协程的博文,里面详细介绍了PHP协程 的实现——传送门

线程与进程的区别

  1. 线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间。
  2. 进程是资源分配和拥有的单元,同一个进程内的线程共享进程的资源。
  3. 线城市处理器调度的基本单位,进程不是。
  4. 两者均可并发执行
  5. 每个独立的线程有一个程序运行的入口,顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

线程和协程的区别

  1. 一个线程可以有多个协程,一个协程也可以单独拥有多个协程
  2. 线程进程都是同步机制,而协程这是异步
  3. 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。

什么是多进程,多线程

多进程是指同一时间里,统一计算机系统中如果允许两个或者两个以上的进程处于运行状态,就是多进程。多开一个进程,多分配一份资源,进程间通信不方便。

多线程就是把一个进程分为很多片,每一片都可以是一个独立的流程与多进程的却别是只会使用一个进程的资源,线程间可以直接通信。

同步阻塞模型

在最早的服务器端程序透视通过多进程、多线程来解决并发IO的问题。

一个请求创建一个进程,然后子进程进入循环同步阻塞地与客户端进行交互,收发处理数据。

多线程模式实现非常简单,线程可以直接向某一个客户端连接发送数据。

步骤:

  1. 创建一个 socket,绑定服务器端口(bind),监听端口(listen),在PHP中用stream_socket_server一个函数就能完成上面3个步骤,当然也可以使用更底层的sockets扩展分别实现。
  2. 进入while循环,阻塞在accept操作上,等待客户端连接进入。此时程序会进入睡眠状态,直到有新的客户端发起connect到服务器,操作系统会唤醒此进程。accept函数返回客户端连接的socket
  3. 主进程在多进程模型下通过fork(PHP: pcntl_fork)创建子进程,多线程模型下使用pthread_create(PHP: new Thread)创建子线程。下文如无特殊声明将使用进程同时表示进程/线程。
  4. 子进程创建成功后进入while循环,阻塞在recv(PHP: fread)调用上,等待客户端向服务器发送数据。收到数据后服务器程序进行处理然后使用send(PHP: fwrite)向客户端发送响应。长连接的服务会持续与客户端交互,而短连接服务一般收到响应就会close。
  5. 当客户端连接关闭时,子进程/线程退出并销毁所有资源。主进程/线程会回收掉此子进程/线程。

缺点:

  • 这种模型严重依赖进程的数量解决并发问题,一个客户端连接就需要占用一个进程,工作进程的数量有多少,并发处理能力就有多少。操作系统可以创建的进程数量是有限的。
  • 启动大量进程会带来额外的进程调度消耗。数百个进程时可能进程上下文切换调度消耗占CPU不到1%可以忽略不计,如果启动数千甚至数万个进程,消耗就会直线上升。调度消耗可能占到 CPU 的百分之几十甚至 100%。

另外有一些场景多进程模型无法解决,比如即时聊天程序(IM),一台服务器要同时维持上万甚至几十万上百万的连接(经典的C10K问题),多进程模型就力不从心了。

还有一种场景也是多进程模型的软肋。通常Web服务器启动100个进程,如果一个请求消耗100 ms,100个进程可以提供1000 QPS,这样的处理能力还是不错的。但是如果请求内要调用外网HTTP接口,像QQ、微博登录,耗时会很长,一个请求需要10s。那一个进程1秒只能处理0.1个请求,100个进程只能达到QPS,这样的处理能力就太差了。

//创建scoket监听
$scokserv = stream_scoket_server('tcp://0.0.0.0:8880', $errno, $errstr);
for ($i = 0; $i < 5; $i++)
{
    if(pcntl_fork() == 0) {
        while (true) {
            $conn = stream_scoket_accept($sockserv);
            if ($conn == false) {
                continue;
            }
            $request = fread($conn, 9000);
            $response = 'hello';
            fwrite($conn, $response);
            fclose($conn);
        }
        exit();
    } 
}

异步非阻塞模型

现在各种高并发异步IO的服务器程序都是基于 epoll 实现的。

在早期Linux就提供了select,可以在一个进程内维持1024个连接,后来加入了poll,可以维持任意数量个连接。

但是poll需要循环检测是否有事件,如果服务器当前有100 W个连接,但是某一个时间内只有一条连接向服务器发送数据,这样系统就会循环100 W次,对于CPU是一种浪费。

Linux在2.6时提供了epoll,可以在系统内维持无数个连接,而且无需轮训。

IO复用异步非阻塞程序使用经典的 Reactor 模型,Reactor 顾名思义,就是反应堆的意思,它本身不处理任何数据收发,只是可以监视一个socket句柄的事件变化。

Reactor模型:

  • Add: 添加一个 SOCKET到 Reactor
  • Set: 修改 SOCKET 对应的事件,如可读可写
  • Del: 从 Reactor 中移除
  • Callback: 事件发生后回调指定的函数

PHP 并发编程实践

PHP的Swoole扩展

PHP的异步、并行、高性能的网络通信引擎,使用纯C语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP 网络客户端,异步 MySQL,异步 Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步 DNS 查询。

除了异步IO的支持之外,Swoole 为 PHP 多进程的模式设计了多个并发数据结构和 IPC 通信机制,可以大大简化多进程并发编程的工作。

Swoole 2.0支持了类似Go语言的协程,可以使用完全同步的代码实现异步程序。

#### Swoole 的异步MySQL实现

$db = new swoole_mysql;
$server = array(
    'host' => '192.168.56.102',
    'port' => 3306,
    'user' => 'test',
    'password' => 'test',
    'database' => 'test',
    'charset' => 'utf8', //指定字符集
    'timeout' => 2,  // 可选:连接超时时间(非查询超时时间),默认为SW_MYSQL_CONNECT_TIMEOUT(1.0)
);

$db->connect($server, function ($db, $r) {
    if ($r === false) {
        var_dump($db->connect_errno, $db->connect_error);
        die;
    }
    $sql = 'show tables';
    $db->query($sql, function(swoole_mysql $db, $r) {
        if ($r === false)
        {
            var_dump($db->error, $db->errno);
        }
        elseif ($r === true )
        {
            var_dump($db->affected_rows, $db->insert_id);
        }
        var_dump($r);
        $db->close();
    });
});

消息队列

用户注册

场景说明:当用户注册后,需要发注册邮件和注册短信。

串行方式:将注册信息写入数据库成功以后,发送注册邮件,再发送注册短信。

并行方式:将注册写入数据库成功后,发送注册邮件的同事,发送注册短信。

消息队列方式:将注册信息写入数据库成功后,将成功信息写入队列,此时直接返回成功给用户,写入队列的时间非常短,可以忽略不计,然后异步发送邮件和短信。

解耦操作

场景说明:用户下单后,订单系统需要通知库存系统。假如库存系统无法访问,则订单减库存将失败,从而导致订单失败。此时需要进行解耦。

引入队列:

  1. 用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户下单成功。
  2. 库存系统订阅下单的消息,采用拉/推得方式,获取下单信息,库存系统根据下单信息,进行库存操作。

流量削峰

应用场景:秒杀活动,流量瞬间激增,服务器压力大

用户发起请求,服务器接收后,先写入消息队列,假如消息队列长度超过最大值,则直接报错或提示用户。

后续程序读取消息队列,在进行处理。

日志处理

应用场景:解决大量日志的传输

日志采集程序可以将日志写入消息队列,然后通过日志处理程序的订阅消费日志。

消息通讯

应用场景: 聊天室

多个客户端订阅同一主题,进行消息发布和接收。

常见的消息队列产品

kafka、ActiveMQ、ZeroMQ, RabbitMQ、Redis等等。

接口的并发请求

curl_multi系列函数。

maksim
Maksim(一笑,吡罗),PHPer,Goper
OωO
开启隐私评论,您的评论仅作者和评论双方可见