PHP 异步与多线程 从 TrueAsync 展望未来

PHP 异步与多线程 从 TrueAsync 展望未来

RFC TrueAsync 1.7 讨论中有个问题:这个提议会如何与 PHP 核心未来的变化互动?要设计好语言的长期演进,至少得对 PHP 的发展方向有基本判断。本文试图回答这个问题。

TrueAsync 项目不仅是 PHP 核心的 async 改动,还包括回答以下问题所需的其他研究:

  • PHP 能在多大程度上向多线程方向发展?
  • 是否存在根本性限制?
  • 实现真正的多线程可能需要哪些核心改动?
  • 可以实现哪些语言抽象?

本文不是 PHP 多线程的详尽综述,也不追求每个细节的技术精确性或大众可读性。但希望它对 PHP 开发者有参考价值,能为后续讨论提供方向。

原文 PHP 异步与多线程 从 TrueAsync 展望未来

历史

几年前要给 PHP 应用加高容量遥测,我说做不到。看到 Swoole 架构后想测试一下。能不能做一个 API,生成和处理大量数据的同时不拖慢客户端?

我们给 PHP 做了个优化的 OpenTelemetry,分批写数据,收集成大块再发到中间遥测服务器。数据压缩,JSON 结构用 MessagePack 序列化。

假设是:用单线程协程逐步构建遥测数据,定时或达到阈值时发送。没有跨线程交互,代码应该快。真的吗?

实验结果:遥测让 API 吞吐量减半。假设错了。为什么?概念上看起来没问题。Swoole 已经让 PHP 函数非阻塞,协程应该高效才对。哪里出错了?

第二版改成:遥测只在一个请求期间收集,立即扔给作业进程去聚合、压缩、发送。这版性能好多了。但不应该啊?进程间数据走管道,一端序列化另一端反序列化。管道在内存里,但系统调用开销不小。

后来找到原因:遥测数据量大,压缩相对 API 请求处理吃掉太多 CPU。Swoole 协程对 I/O 高效,但帮不了 CPU 密集型任务。

这个案例说明单线程协程解决不了所有问题。也说明多线程能补充协程,给更多问题提供工具。

单线程 + offload

把 CPU 密集型工作卸载到单独进程不是什么新发明。这个模式在不同语言和框架中独立出现,叫 Single-threaded + offload

打个比方:一个人快速分拣信件(每小时几千封),重包裹由其他员工用卡车装走。分拣员要是自己去搬包裹,信件队列就堆到天花板了。

Single-threaded + offload 模型把任务分两类:

  • I/O-bound 任务 --- 读文件、网络调用、数据库访问。大部分时间在等外部世界。通过并发 async(协程、await),一个线程能容纳几千个这类操作。
  • CPU-bound 任务 --- 压缩、加密、解析、计算。CPU 满负荷跑,光靠并发不够,得上更多核心。

模型在物理上分离这两类任务:主线程(Event Loop)只管 I/O,CPU 任务扔给单独的线程或进程(Workers)。

Node.js 靠单线程 Event Loop 出名,很适合网络应用。但开发者要是在请求处理器里直接处理图像或压缩视频,服务器就变南瓜了。后来加了 Worker Threads,专门跑 CPU 密集型操作。

Python 走了类似路线。asyncio 出来后,I/O-bound 代码有了好工具,但 GIL(全局解释器锁)挡着,单进程内没法真正 CPU 并行(写这篇文章时问题已解决)。阻塞操作有 loop.run_in_executor()asyncio.to_thread()(Python 3.9+),把重活卸载到线程池或进程池。Event Loop 保持响应,计算并行跑。

PHP/Swoole 也是这个架构:Request Workers 用协程处理 HTTP 请求,Task Workers 跑重计算。通过 UnixSocket 或管道通信,单进程能处理每秒约 10 万次操作。

模型的优势

1. 资源效率

单线程 Event Loop 能以极小开销服务几千个并发 I/O 操作。协程任务间切换比操作系统线程上下文切换便宜得多。CPU-bound 任务在多核上真正并行------每个 worker 占一个核心,互不干扰。

2. 开发简单

Event Loop 里的代码不用互斥锁、信号量这些多线程编程的玩意儿。单线程模型一次只跑一个任务,竞态条件不可能出现。Workers 并行跑,但只要遵循 Shared Nothing,同步问题就不存在。

多线程代码和单线程 async 代码的复杂度差距很大。现代语言和框架都奔着单线程 async 去,不走经典多线程。

3. 编译器/运行时更简单

单线程模型的 async 函数对编译器和运行时简单得多。好的多线程语言需要自己的代码生成管道。PHP 有个硬约束:部分代码用 C 写的,没法对线程、内存管理、参数传递做高效的字节码级优化。Go 的设计复杂出名:专有栈、复杂 GC,都是高效 goroutines 和 channels 必需的。PHP 的 GC 后面会讲,先别放松。

4. 手动负载分配

开发者可以主动在请求处理代码和 worker 池之间分配负载。手动控制能从硬件榨出理论最大值。但这也是缺点。

模型的劣势

1. 手动负载分配

手动分配是双刃剑。开发者可以针对特定任务优化,也可能误判什么该放 I/O 代码、什么该放 workers。I/O 代码塞了重活会过载,响应变慢、延迟上升。

这个模型要求 PHP 开发者有足够技能,或者依赖框架作者提供的现成方案。

2. 不适合所有任务

Single-threaded + offload 很适合 Web 服务器、API、微服务------主要负载是数据库、文件系统、网络调用这些 I/O。但每一步都要密集计算的场景------科学计算、渲染、机器学习------这个模型效果就差了。那些场景更适合完全多线程。

你可能说:能接受!准备好了!但 PHP 本身准备好多线程了吗?

PHP 准备好多线程了吗?

开发 TrueAsync 时,最难的讨论之一是"为什么 PHP 没有 async"。解释 PHP 为什么还没准备好多线程可能同样难。不过先聊聊多线程本身。我们为什么需要它?或者换个问法:我们为什么不需要它?

多线程不是并行执行代码的必要条件。

"并行必须多线程"这个想法早就刻在程序员脑子里了,就像"黑洞会吸东西"刻在流行文化里一样。

并行执行完全可以用进程,进程彼此隔离(80386 架构就有了)。进程通过 IPC 通信,完成状态通过信号(操作系统事件)跟踪。那为什么还要线程?

要老实回答这个问题,得穿越回去请当年做决定的人解释:Edsger Dijkstra、Fernando Corbató、Barbara Liskov、Richard Rashid。办个脱口秀挺好。但就算他们都来了,可能也说不出直接答案。

下面这个说法是错的:

线程是为了让并行代码不用额外工具就能共享内存。

进程也能共享内存,但得把段映射到地址空间(额外工具)。线程默认共享所有内存。线程 A 能访问的变量 x,线程 B 在同一地址也能访问,不用任何技巧......但等等!多个线程没法在不加额外工具的情况下安全使用共享变量。

更准确的说法是:

线程是为了在任务间传递内存时没有额外开销。

如果线程用内存传消息,保证同一时间只有一个线程能访问某块内存区域,那在内存和 CPU 两方面都是最高效的。线程刻意避免共享内存。这个模型叫 Shared Nothing

线程是为了在任务间高效传数据。 跟"黑洞不吸东西"一样是事实。

PHP 内存模型

PHP 怎么处理内存?简化的抽象模型:

  • 代码
  • 数据
  • PHP VM 状态

线程间共享 PHP 代码已经能做到(PHP JIT 解决了)。其他组件紧密耦合,拆不开。比如 PHP 用一个全局 object_store 存所有创建对象的引用。PHP 内存管理器是给单个 PHP VM 的对象设计的,不面向多线程。PHP 垃圾收集器处理不了不同线程的数据,甚至要完全停掉 VM,因为它直接改对象的 refcount

所以 PHP 是严格的单线程模型,带 stop-the-world GC

在线程间移动 PHP VM

PHP 用线程局部存储(Thread-Local Storage,TLS)保存每个线程的 VM 状态。这对 ZTS(Zend Thread Safety)模式下线程间隔离很关键。

现代 PHP 构建用 C11 标准的 __thread(MSVC 里是 __declspec(thread))获取 VM 状态指针。速度很快,x86_64 上就是从 FS 或 GS 寄存器的基址读一个偏移量。

asm 复制代码
; offset - 编译时计算的常量偏移量
; fs - 内存段的基地址
mov rax, QWORD PTR fs:offset

FS/GS 对每个线程唯一(操作系统保证),读出来的总是正确的 VM 状态指针。

能在线程间移动 VM 状态,就能实现类似 Go 协程或 actors 的功能。现代 VM 通过自定义代码生成传上下文,用 CPU 寄存器传 VM 状态。PHP 做不到这个,因为底层用 C 函数,C 没法给每个函数传隐式上下文参数。在线程间移动 PHP VM 状态会损失一定性能。

但如果只移动执行代码需要的那一小部分 VM 状态呢?比如 PHP Fiber 切换时会复制指向全局结构(zend_executor_globals)的部分指针。

如果把 PHP VM 概念上分成两部分:

  • PHP VM shared。类、函数、常量、ini 指令、可执行代码。
  • PHP VM movable。需要移动的 VM 部分。

有些结构可以标记为共享,有些标记为可移动;Executor Globals 甚至可以拆成共享和可移动两部分,实现线程间高效的 VM 状态移动。扩展全局结构不会因为多一层间接而损失性能,因为它们本来就在用间接访问。

问题出在代码编译相关的结构上。PHP 通过 include/requireeval 和自动加载是动态的,这让 VM 状态很难有效拆成共享和可移动两部分。如果能解决这个问题,PHP 就能以很小的开销在线程间移动部分 VM 状态。

在线程间传递对象

PHP 要改什么才能安全地在线程间传递对象?怎么做?

从语言层面看。假设 $obj 里有个 SomeObject 实例,要发到另一个线程。能做到吗?

php 复制代码
$obj = new SomeObject();

$thread = new Thread(function () use ($obj) {
    echo $obj->someMethod();
});

$thread->join();

SomeObject 只属于 $obj,可以安全地把地址从一个线程移到另一个。主线程的 $obj 会被销毁:

php 复制代码
$obj = new SomeObject();
$thread = new Thread(function () use ($obj) {
    echo $obj->someMethod();
});

// $obj 在这里未定义
$thread->join();

上面的代码跟 C++ 和 Rust 的移动语义完全一样。这种线程间传内存的方式:

  • 安全。只有一个线程拥有对象。
  • 没有复制或序列化开销

为了让行为可预测、静态分析器能读懂,应该加特殊的移动语法:

php 复制代码
$obj = new SomeObject();

// consume $obj 表示移动对象
$thread = new Thread(function () use (consume $obj) {
    echo $obj->someMethod();
});

// $obj 在这里未定义。在 PHP9 中应该在这里报告错误。
echo $obj;

看着不错?

但移动 refcount = 1 的对象有问题。

看个分类树的例子:

php 复制代码
$electronics = new CategoryNode('Electronics');
$categoriesTree = new Tree();
$categoriesTree->addToPath('/products/electronics', $electronics);
$categoriesTree->addToPath('/popular/electronics', $electronics);  
// 同一个分类!

$electronics 在树里出现两次(refcount = 2)。把 $categoriesTree 移到另一个线程会怎样?

要安全移动,必须保证图里所有对象都没有外部引用:

php 复制代码
$node = new CategoryNode('Electronics');
$categoriesTree = new Tree();
$categoriesTree->addToPath('/products/electronics', $node);

$favourites = [$node];  // 外部引用!
$thread = new Thread(function () use ($categoriesTree) {
    // $categoriesTree 被移动
});

// $favourites[0] 现在指向另一个线程中的内存
// 悬空指针!

安全移动需要:

  • 完整图遍历:检查所有嵌套对象。
  • Refcount 检查:图里每个对象都要查。
  • 身份保留:图内的重复项得保持重复。

可以为此设计算法,叫深拷贝。简单实现大概这样:

php 复制代码
// 深拷贝伪代码
// 线程 A 中的源图
$node = new Node('A');        // addr: 0x1000
$tree->left = $node;          // addr: 0x1000
$tree->right = $node;         // addr: 0x1000 (相同引用)

// 深拷贝到线程 B(带 MM 的伪代码)
$copied_map = [];  // 哈希表: addr_source -> addr_target
function deepCopyToThread(object $obj, Thread $target_thread_mm) 
{
    $source_addr = get_object_address($obj);
    if (isset($copied_map[$source_addr])) {
        return $copied_map[$source_addr];  // 已经复制!
    }
    // 在另一个线程的 MM 中分配内存
    $new_addr = $target_thread_mm->allocate(sizeof($obj));
    $copied_map[$source_addr] = $new_addr;
    // 复制对象数据
    memcpy($new_addr, $source_addr, sizeof($obj));
    // 遍历属性
    foreach ($obj->properties as $prop) {
        if (is_object($prop)) {
            $new_prop_addr = deepCopyToThread($prop, $target_thread_mm);
            // 更新新对象中的指针
            update_property($new_addr, $prop, $new_prop_addr);
        }
    }
    return $new_addr;
}
// 线程 B 中的结果:
// $newTree->left (addr: 0x2500) === $newTree->right (addr: 0x2500)
// 身份保留!

深拷贝时间复杂度 O(N + E),N 是对象数,E 是引用数。空间复杂度 O(N)------哈希表 + 新对象 + 递归栈。

比序列化快,因为不用转换传输格式,但收益取决于数据形状和图大小。也可以混合:refcount = 1 的移动,其他的深拷贝。

结果:

  • PHP 开发者不用管对象怎么传到另一个线程。
  • 最好情况:内存直接移动(refcount = 1)。
  • 最坏情况:深拷贝,保留身份(refcount > 1)。

看着还行:

  • PHP 语法改动最小
  • 可以逐步改
  • 多线程能用了

但核心层面没那么美好。要让对象移动成真,PHP 需要跨线程的内存管理机制。现在做不到。

多线程 PHP 内存管理器

PHP 内存管理器类似 jemalloc 或 tcmalloc 这些现代分配器。区别是:它没有从另一个线程释放内存的正确算法。

场景:

  • 线程 A 创建对象。
  • 移动(原样)给线程 B。
  • B 不再需要,要释放。

每个 PHP 线程有自己的内存管理器(Memory Manager,MM)。B 想释放 A 分配的内存就出问题了。B 的 MM 不认识 A 的内存,释放会出错。B 直接访问 A 的 MM 结构也不行,需要同步。现代高性能多线程分配器用延迟释放(deferred free)解决这个问题。

延迟释放的思路:

  • B 的 MM 看到一个不认识的指针。
  • 找到哪个 MM 拥有它,给那个 MM 的队列发消息说可以释放了。
  • A 的 MM 处理队列,在自己的上下文里释放。

用现代无锁结构,这个算法吞吐量高,不同线程能并行释放内存,几乎不用锁。

多线程 PHP 内存管理器为以前不可能的改动打开了门。

共享对象

能用最少操作把内存从一个线程传到另一个很好,但如果能创建一开始就设计成跨线程共享的对象呢?

很多服务可以构建成不可变对象,应该能在进程间共享,省内存、加快 worker 启动。

refcount 挡着,它让所有 PHP 对象实际上都是可变的。能绕过吗?

代理对象

第一种方法是代理对象,引用存在所有线程可访问的共享内存池里的真实对象。代理只存标识符或指针,加上访问数据的方法。缺点:

  • 访问数据/属性变慢
  • Reflection API 和类型检查更复杂

PHP 已经有强大的代理机制。代理共享对象在某些场景不错,比如计数器表或 Swoole/Table 这样的数据表。

带有 GC_SHARE 标志的共享对象

PHP 有个内置机制通过 GC_IMMUTABLE 标志实现不可变元素,用于:

  • 内部字符串(IS_STR_INTERNED)------整个 PHP 进程存在的字符串常量
  • 不可变数组(IS_ARRAY_IMMUTABLE)------比如 zend_empty_array
  • opcache 里的常量------带常量数据的编译代码

GC_IMMUTABLE 让引擎跳过这些结构的 refcount 修改:

c 复制代码
// Zend/zend_types.h
// 为 zend_refcounted_h 增加 refcount 的函数
static zend_always_inline void zend_gc_try_addref(zend_refcounted_h *p) {
    if (!(p->u.type_info & GC_IMMUTABLE)) {
        ZEND_RC_MOD_CHECK(p);
        ++p->refcount;
    }
}

类似机制可以支持 SharedObjects,比如加个 GC_SHARE 标志。

性能分析显示,检查 GC_SHARE 给单独的 refcount++ 加了 +34% 开销(微基准测试)。实际应用里 refcount 操作只占总工作一小部分,影响几乎看不出来:

  • 真实操作(数组/对象):+3--9%
  • 实际应用:+0.05--0.5%

这解决了一半问题;另一半是给这些对象设计 GC。用原子 refcount 不理想,多线程访问同一对象时会变慢。延迟释放算法可能更合适。

基于区域的内存

基于区域的内存(Region-based memory)在面向 Web 的语言里越来越流行。

思路:给特定任务或线程在单独区域分配内存,不需要时整体释放。避免了逐个对象管理的复杂性,GC 也简单了。

比如 PHP MM 可以保证对象在绑定到特定 PHP 对象的区域里创建。区域生命周期等于对象生命周期。

对象销毁时整个区域直接释放,不用遍历子对象。这种对象要"移动"到另一个线程,可以避免深拷贝。

PHP VM 实现基于区域的内存有问题:比如全局对象列表、操作码缓存。但高效实现的机会不是零,值得继续研究。

有效的基于区域的内存算法能为 actors 打开门------有隔离内存的特殊对象。

Actors 是多线程编程里最方便、最强大、最安全的工具。

协程和线程协作

从协程角度看,Thread 是个 Awaitable 对象。协程可以等 Thread 结果而不阻塞其他协程。一个线程能托管很多等重任务的协程。服务它们的线程对新请求保持快速响应,因为等 Thread 不阻塞 Event Loop

php 复制代码
use Async\await;
use Async\Thread;

$thread = new Thread(function() {
    // 硬件密集型任务在这里
    return 42;
});

$result = await($thread); 
// 协程在这里暂停,直到 Thread 完成

这种方式能实现有 CPU 密集型任务和简单业务逻辑的聊天场景。

图里是个示例架构。应用有两个线程池:带并发多任务的请求处理线程,和跑 CPU 密集型任务的 worker 线程。协程处理请求,worker 跑重任务时可以完全暂停,跑完继续。

php 复制代码
use Async\await;
use Async\ThreadPool;

final readonly class ImageDto
{
    public function __construct(
    public int $width,
    public int $height,
    public string $text,
) {}
}

$pool = new ThreadPool(2);
$dto = new ImageDto(
    width: 200,
    height: 200,
    text: 'Hello TrueAsync!'
);

$image = $pool->enqueue(function (ImageDto $dto) {
    $img = imagecreatetruecolor($dto->width, $dto->height);

    $white = imagecolorallocate($img, 255, 255, 255);
    $black = imagecolorallocate($img, 0, 0, 0);

    imagefill($img, 0, 0, $white);
    imagestring($img, 5, 20, 90, $dto->text, $black);

    ob_start();
    imagepng($img);
    imagedestroy($img);
    return ob_get_clean();
}, $dto);

$response->setHeader('Content-Type', 'image/png');
$response->write($image);
$response->end();

协程代码是顺序的,读起来像普通代码,ThreadPool::enqueue 像在同一线程调用回调一样。DTO 跨线程传,结果字符串不会在内存里复制两次。

垃圾收集器和有状态模式

现代化 PHP 内存管理器不是改进多线程环境唯一要做的事。没有高效 GC,多线程 PHP 会有性能问题和循环引用导致的内存泄漏。

PHP GC 用两种算法:引用计数做主要内存管理,并发循环收集(Concurrent Cycle Collection,Bacon-Rajan,2001)处理循环。引用计数每次赋值都递增/递减,没同步的话多线程不安全。每次赋值用原子操作开销太大;不同步就有竞态和泄漏。循环收集器虽然叫"并发",但只在单线程内工作,用颜色标记(PURPLE → GREY → WHITE/BLACK)找循环,也不是线程安全的。

好消息是:当前 GC 实现在多线程环境能工作,因为它跟内存管理器分开,不依赖内存在哪分配。

但 PHP 要进入有状态应用的多线程时代,GC 得适应:

  • 在单独线程并行跑,不影响业务代码。
  • 尽快释放资源。
  • 提供泄漏检测、日志、遥测的额外工具(长时间运行的应用特别需要)。

循环收集器可以改成在多线程环境工作,在单独线程处理引用,提高整体响应性。这可能够用了!

Actors

ThreadPool 和线程间传对象有用,但需要开发者的注意力、技能和精力。有个更好的多线程编程抽象,藏住线程/内存复杂性,完美契合业务逻辑:actors。

Actors 是并发并行编程模型,计算的基本单元是 actor。

每个 actor:

  • 有自己的隔离状态
  • 顺序处理消息
  • 只通过消息跟其他 actors 交互
  • 可能在单独线程跑

可以把 actor 想成对象,这让多线程 PHP 能用熟悉的 OOP 模式。

想象一个有很多房间的聊天服务器。每个房间是单独的对象。

php 复制代码
use Async\Actor;

class ChatRoom extends Actor
{
    private array $messages = [];
    private string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function postMessage(string $user, string $text): void
    {
        $this->messages[] = [
            'user' => $user,
            'text' => $text,
            'time' => time()
        ];
    }

    public function getMessages(): array
    {
        return $this->messages;
    }
}

spawn(function() {
   $room = new ChatRoom('general');
   $room->postMessage('Alice', 'Hello!');  // 在另一个线程中运行,暂停协程!
   $messages = $room->getMessages();       // 在另一个线程中运行,暂停协程!
   echo json_encode($messages);
});

ChatRoom 对象特殊。它们的数据和 PHP VM 状态本地化了,方便在线程间移动。每个方法在自己的线程跑,但任何时刻只有一个线程能执行给定 actor 的方法。

语义上,基类 Actor 定义了 PHP VM 和内存管理器的工作方式,让 ChatRoom 对象能安全地在单独线程跑。类类型不只"存"方法和属性信息,还存 MM 和 GC 该怎么操作这类对象。Rust、C++ 也有类似做法。好处:不改语法,符合 OOP 哲学。

示例看起来像协程里跑的普通顺序代码。但 postMessagegetMessages 在另一个线程跑,不会直接执行。协程给 actor 队列发消息,进入等待,actor 在另一个线程跑完方法返回结果后才恢复。

这跟熟悉的 PHP OOP 不冲突:Actor 重写 __call

php 复制代码
class Actor 
{
    private $threadPool;

    public function __call(string $name, array $arguments): mixed
    {
        if(current_thread_id() === $this->threadPool->getThreadIdForActor($this)) {
            // 如果我们在同一个线程中,直接运行方法
            return $this->$name(...$arguments);
        }
    
        // 否则将调用排队给 actor
        return $this->threadPool->enqueueActorMethod($this, $name, $arguments);
    }
}

enqueueActorMethodpostMessage 加到 actor 队列,订阅结果事件,调用 Async\suspend() 暂停协程。

Actor 代码顺序执行,解决竞态条件,多线程开发对开发者透明。

并行性靠每个 ChatRoom actor 能在单独线程跑来实现:

php 复制代码
spawn(function() {
   $room = new ChatRoom('room1');
   $room->postMessage('Alice', 'Hello!');
   $messages = $room->getMessages();
   echo json_encode($messages);
});

spawn(function() {
   $room = new ChatRoom('room2');
   $room->postMessage('Bob', 'Hi there!');
   $messages = $room->getMessages();
   echo json_encode($messages);
});

ChatRoom 实例能在不同线程并行跑,因为每个 actor 有自己的执行线程、唯一的 PHP VM 状态和内存。

创建 100 个聊天室:

php 复制代码
use Async\Actor;

$rooms = [
    'general' => new ChatRoom('general'),
    'random'  => new ChatRoom('random'),
    'tech'    => new ChatRoom('tech'),
    // ... 97 个更多房间
];

// 处理请求的协程
HttpServer::onRequest(function(Request $request, Response $response) use ($rooms) {
   // HTTP 请求处理
   $roomName = $request->getQueryParam('room');
   $room = $rooms[$roomName] ?? null;
   
   if (!$room) {
      $response->setStatus(404);
      $response->write('Room not found');
      $response->end();
      return;
   }
   
   // 调用看起来是同步的,但在另一个线程中运行!
   $room->postMessage($request->getQueryParam('user'), $request->getQueryParam('text'));
   $messages = $room->getMessages();
   
   $response->setHeader('Content-Type',  'application/json');  
   $response->write(json_encode($messages));
   $response->end();
});

每个聊天室顺序处理消息,跟其他房间并行。

Actors 不需要互斥锁、锁、复杂同步或手动线程池交互。它们是现成的高级并行化方案。

一个聊天室要给另一个发消息也行,因为 actors 是 SharedObject,能跨线程交互:

php 复制代码
class Rooms extends Actor
{
    private array $rooms = [];
    
    public function __construct(string ...$roomNames)
    {
       foreach ($roomNames as $name) {
           $this->rooms[$name] = new ChatRoom($name);
       }
    }
    
    public function broadcastMessage(string $fromRoom, string $user, string $text): void
    {
        foreach ($this->rooms as $name => $room) {
            if ($name !== $fromRoom) {
                // 非阻塞调用
                $room->postMessageAsync($user, $text);
            }
        }
    }
}

spawn(function() {
   $rooms = new Rooms('general', 'room1', 'room2', 'room3');
   $rooms->broadcastMessage('general', 'Alice', 'Hello!');
});

Actor 内部

PHP VM 保证 actor 内所有对象:

  • 要么只属于该 actor,在其唯一区域分配
  • 要么从其他区域或线程移动过来
  • 要么是另一个 SharedObject 或另一个 actor

Actor 要么拥有自己的区域,要么只用显式共享的不可变对象;否则竞态还是会有。

内存管理器保证 actor 方法内所有内存操作自动绑定到与 actor 关联的区域。

方法通过 Scheduler 服务的 MPMC 消息队列执行。Scheduler 在 actors 间分配 CPU 时间,提供并发和并行执行。

结论

这些听着都不错,但什么时候能真正用上?

Single-threaded + offload 模型可能很快出现,很多组件已经就绪。TrueAsync 单线程协程已到 beta 版。实验性的多线程内存管理器和创建线程的 API 已经实现。

Actors 需要更多开发时间,因为涉及 PHP 核心很多部分,但仍是 PHP 9 的现实目标,给市场提供一种安全的多线程编程语言。

相关推荐
BingoGo12 小时前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack12 小时前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack1 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo1 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack3 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理3 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
QQ5110082853 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php
WeiXin_DZbishe3 天前
基于django在线音乐数据采集的设计与实现-计算机毕设 附源码 22647
javascript·spring boot·mysql·django·node.js·php·html5
longxiangam3 天前
Composer 私有仓库搭建
php·composer
上海云盾-高防顾问3 天前
DNS异常怎么办?快速排查+解决指南
开发语言·php