线程安全
内存溢出 PHP本身(无论是PHP-FPM还是Swoole)并不支持多线程。 线程安全,在PHP的世界里通常指以下两种情况,我们需要分开讨论: PHP解释器自身的线程安全:指PHP作为C语言编写的程序,当其被嵌入到多线程的Web服务器(如Apache的worker MPM)中时,多个线程能否安全地共用一个PHP解释器实例。 PHP应用代码的并发安全:指我们的业务代码在同时处理多个请求时,如何避免共享数据的冲突。这在Swoole这类常驻内存的进程中尤其重要。 下面分别介绍: 1. PHP解释器的线程安全(Zend线程安全,ZTS) 这是PHP源码编译时的一个选项(–enable-zts)。 问题背景:PHP有很多全局变量(例如存放已加载扩展列表的变量)。在单线程环境下没问题,但在多线程环境下,线程A修改这个全局变量时,线程B可能正在读取,导致崩溃。 解决方案(ZTS):PHP通过TSRM(线程安全资源管理器) 为每个线程分配了一个独立的“全局变量副本”。线程A修改自己的副本,不会影响线程B。 对开发者的影响: PHP-FPM(Nginx+PHP-FPM):使用的是多进程模型,一个进程只处理一个请求。进程间内存天然隔离,不存在线程安全问题,因此通常编译非线程安全(NTS) 版本,性能更高。 Apache + worker MPM:Apache使用多线程模型,此时PHP必须编译为ZTS版本才能稳定运行。 Swoole:Swoole本身是多进程模型。每个Worker进程是单线程的,事件循环和协程都在这个主线程中运行。因此,PHP的ZTS在Swoole环境下并非必需,Swoole官网也推荐使用NTS版本的PHP。 小结:对于绝大多数PHP开发者(使用PHP-FPM或Swoole),无需关心PHP底层的ZTS。你基本不会直接编写相关代码,这是扩展开发者和PHP本身需要解决的问题。 2. 应用代码的并发安全:Swoole常驻内存下的挑战 在传统的PHP-FPM中,每个请求结束后,所有变量都被销毁,下一个请求是“干净”的,所以不存在并发安全问题。 但在Swoole中,Worker进程常驻内存,多个协程或请求会并发地操作进程内的同一个全局变量、静态变量或资源(如Redis连接),这就会引发经典的竞态条件(Race Condition)。 典型问题场景: // 一个 Worker 进程内的全局计数器 $globalCounter = 0; // 协程A 和 协程B 同时执行这段代码 $globalCounter++; // 这不是原子操作! echo $globalCounter; 两个协程同时读取0,分别加1,然后写回。你期望输出2,但实际可能输出1。这就是并发问题。 如何保证Swoole下的并发安全? Swoole提供了几种机制来解决这个问题: 方案一:使用原子计数器(Atomic) Swoole提供的Swoole\Atomic是一个基于CPU原子操作的计数器,操作是线程/协程安全的。 $atomic = new Swoole\Atomic(0); // 在任意协程中安全地增加 $atomic->add(); // 无锁,高性能 echo $atomic->get(); 方案二:使用锁(Lock) 对于复杂的共享数据读写(不只是计数),可以使用各种锁。 协程锁(Swoole\Lock 或 Channel):推荐,只在协程级别阻塞。 $lock = new Swoole\Lock(Swoole\Lock::SPIN); // 自旋锁 $lock->lock(); // ....