当前位置: 首页 > news >正文

异步 PHP — 多进程、多线程和协程

让我们看一下这段典型的 PHP 代码:

function names()
{
    $data = Http::get('data.location/products')->json();

    $names = [];

    foreach ($data as $item){
        $names[] = $item['name'];
    }

    return $names;
}

我们发送一个返回项目数组的 HTTP 请求,然后我们将每个项目的名称存储在一个 $names 数组中。

执行此函数所花费的时间等于请求的持续时间加上构建数组所花费的时间。如果我们想为不同的数据源多次运行这个函数怎么办:

$products = names('/products');
$users = names('/users');

运行此代码所花费的时间等于两个函数组合的持续时间:

HTTP request to collect products: 1.5 seconds
Building products names array:    0.01 seconds
HTTP request to collect users:    3.0 seconds
Building users names array:       0.01 seconds
Total:                            4.52 seconds

这称为同步代码执行,或一次执行一件事。为了使此代码运行得更快,您可能希望异步执行它。那么,如果我们想实现这一目标,我们有哪些选择呢?

  1. 在不同的进程中执行。
  2. 在不同的线程中执行。
  3. 在协程/纤维/绿色线程中执行。

在不同的进程中执行

在单独的进程中运行这些函数调用中的每一个,都会为操作系统提供并行运行它们的任务。如果您有一个多核处理器,我们现在都有,并且周围有 2 个空闲内核,操作系统将使用两个内核并行(同时)执行进程。但是,在大多数情况下,机器上运行的其他进程需要使用可用的内核。在这种情况下,操作系统将在这些进程之间共享 CPU 时间。换句话说,可用内核将在处理我们的两个进程和其他进程之间切换。在这种情况下,我们的进程将同时执行。

这两个进程的执行时间大约是 3.03 秒(我知道不是 100% 准确)。这个结论是基于这样一个事实:最慢的请求需要 3 秒,2 次网络调用需要 10 毫秒,两个用于收集名称的循环各需要 10 毫秒。

核心内部的执行将如下所示:

Switch to process 1
Start HTTP request to collect products
Switch to process 2
Start HTTP request to collect users
Switch to process 1
If a response came, then build products names array
Switch to process 2
If a response came, then build users names array

因此,当 CPU 等待第一个请求的响应时,它会发送第二个请求。然后等到任何请求返回,然后再继续该过程。

多处理是在 PHP 中实现异步代码执行的一种简单方法。但是,它并不是性能最高的。因为创建进程相对昂贵,并且每个进程都需要在内存中拥有自己的私有空间。进程之间的切换(上下文切换)也有开销。

您可以使用Laravel 队列并启动固定数量的工作人员(进程)并让它们保持活动状态以处理您的任务。这样你就不必每次想要异步运行时都创建新进程。但是,上下文切换和内存分配的开销仍将适用。同样对于工作人员,您需要管理如何从工作人员内部的代码执行中接收结果。

多处理和 Laravel 工作人员已经为数百万个应用程序做得很好。因此,当我说它不是相对于其他选项时性能最好的。不要只是阅读本文并认为多处理和队列不好。好?

在不同的线程中执行

一个进程在内存中有自己的私有空间,一个进程可能有多个线程。所有线程都与进程位于相同的内存空间中。这使得生成线程比生成进程更便宜。

但是,上下文切换仍然会发生。当你有太多线程时,比如有太多进程,你机器上的一切都会变慢。因为 CPU 内核在很多上下文之间切换。

此外,由于多个线程同时访问相同的内存空间,可能会发生争用情况。

除此之外, 不再支持 PHP 中的多线程。

在 coroutines/fibers/green-threads 中执行

这个“东西”有很多名字。但是我们称它为“协程”,因为它是最常用的术语。

协程就像一个线程,它共享它在内部创建的进程的内存,但它不是一个实际的线程,因为操作系统对此一无所知。操作系统级别的协程之间没有上下文切换。运行时控制切换发生的时间,这比 OS 上下文切换成本更低。

让我们将代码转换为使用协程。此代码仅用于演示,如果您运行它将不起作用:

$products = [];
$users = [];

go(fn() => $products = names('/products'));

go(fn() => $users = names('/users'));

协程背后的想法是运行时将安排同时运行这些回调。在每个协程内部,代码可以显式地将控制权交给运行时,以便它可以运行另一个协程。在任何给定时间,只有一个协程正在执行。

所以如果我们分解我们的代码,它会是这样的:

go(function(){
    $data = Http::get('data.location/products')->json();
    // yields

    foreach(...)
});

go(function(){
    $data = Http::get('data.location/users')->json();
    // yields

    foreach(...)
});

运行时将执行第一个协程直到它产生,然后执行第二个协程直到它产生,然后返回到它在第一个协程中停止的位置。直到所有协程都执行完毕。然后,它将以常规同步方式继续执行代码。

现在你可能有两个问题;首先,什么时候应该屈服?第二,我们如何实现它?

在我们的示例中,每个协程内部都有两个操作;一个 I/O 绑定和一个 CPU 绑定。发送 HTTP 请求是一个 I\O 绑定操作,我们发送请求(输入)并等待响应(输出)。另一方面,循环是一个 CPU 密集型操作,我们正在循环一组记录并计算结果。计算是由 CPU 完成的,这就是为什么它被称为 CPU bound。

如果在同一进程中运行,受 CPU 限制的工作将花费相同的时间。使工作花费更少时间的唯一方法是在不同的进程或线程中执行它。另一方面,I\O 绑定的工作可以在同一个进程内并发运行;当一个 I\O 操作正在等待输出时,另一个操作可以开始。

查看我们的示例,运行时内部的执行将如下所示:

Start coroutine 1
Start HTTP request to collect products
Coroutine 1 yields
Switch to coroutine 2
Start HTTP request to collect users
Coroutine 2 yields
Switch to coroutine 1
If a response came, then build products names array
Switch to coroutine 2
If a response came, then build users names array

使用协程,我们可以将 I\O 操作花费在等待上的时间用于做其他工作。通过这样做,我们同时运行所有协程。

现在让我们转向我们的第二个问题:我们如何使屈服发生?我们没有。

不同的框架和库必须通过在 I\O 操作等待时让出控制来支持异步执行。有一个流行的术语,你应该知道“非阻塞 I\O”。与数据库、缓存、文件系统、网络等通信的库必须适应非阻塞。

如果您在协程中使用阻塞库,它将永远不会产生,因此您的协程将同步执行。主进程将等到 I\O 操作收到输出,然后再继续程序的其余部分。

结论

关于协程和异步执行还有很多话要说。我的计划是探索如何让 Laravel 与协程很好地配合,并在此过程中分享我的发现。

在那之前,拥抱 PHP 的同步代码执行。超过 25 年,它运行良好。将您需要剪切的 I\O 绑定工作发送到队列工作程序并稍后对结果采取行动。这应该涵盖许多用例。

 

相关文章:

  • 适用于90%网剧、网大的最新备案流程解析
  • 在PyG上构建自己的数据集
  • Docker部署Logstash 7.2.0
  • Nginx -- -- 配置SSL证书
  • DID革命:详解PoP、SBT和VC三种去中心化身份方案
  • Redis与Python交互
  • 算法基础: 位运算
  • 记录一次坑 | 包版本不一致产生的问题的排查过程
  • SmartX Everoute 如何通过微分段技术实现 “零信任” | 社区成长营分享回顾
  • “相信美好,即将发生”——天泽智云
  • 面试阿里技术专家岗,对答如流,这些面试题你能答出多少
  • Spring AOP与事务
  • 时序与空间结构
  • 一幅长文细学TypeScript(一)——上手
  • DM JDBC
  • 《Javascript高级程序设计 (第三版)》第五章 引用类型
  • Android框架之Volley
  • Cookie 在前端中的实践
  • es6
  • hadoop集群管理系统搭建规划说明
  • Linux编程学习笔记 | Linux多线程学习[2] - 线程的同步
  • Mac转Windows的拯救指南
  • Redux系列x:源码分析
  • Spark VS Hadoop:两大大数据分析系统深度解读
  • Spring Cloud中负载均衡器概览
  • SQLServer之创建数据库快照
  • vue-cli在webpack的配置文件探究
  • 从零开始学习部署
  • 关于 Cirru Editor 存储格式
  • 批量截取pdf文件
  • 漂亮刷新控件-iOS
  • 悄悄地说一个bug
  • 使用parted解决大于2T的磁盘分区
  • 微服务框架lagom
  • 我这样减少了26.5M Java内存!
  • 一个完整Java Web项目背后的密码
  • 在GitHub多个账号上使用不同的SSH的配置方法
  • const的用法,特别是用在函数前面与后面的区别
  • 国内开源镜像站点
  • 说说我为什么看好Spring Cloud Alibaba
  • ​中南建设2022年半年报“韧”字当头,经营性现金流持续为正​
  • # 数论-逆元
  • #[Composer学习笔记]Part1:安装composer并通过composer创建一个项目
  • (+4)2.2UML建模图
  • (01)ORB-SLAM2源码无死角解析-(56) 闭环线程→计算Sim3:理论推导(1)求解s,t
  • (4)(4.6) Triducer
  • (Matlab)基于蝙蝠算法实现电力系统经济调度
  • (Redis使用系列) Springboot 在redis中使用BloomFilter布隆过滤器机制 六
  • (层次遍历)104. 二叉树的最大深度
  • (附源码)springboot学生选课系统 毕业设计 612555
  • (附源码)ssm户外用品商城 毕业设计 112346
  • (力扣)1314.矩阵区域和
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理 第13章 项目资源管理(七)
  • (七)微服务分布式云架构spring cloud - common-service 项目构建过程
  • (完整代码)R语言中利用SVM-RFE机器学习算法筛选关键因子