内存溢出

PHP本身(无论是PHP-FPM还是Swoole)并不支持多线程。

线程安全,在PHP的世界里通常指以下两种情况,我们需要分开讨论:

  1. PHP解释器自身的线程安全:指PHP作为C语言编写的程序,当其被嵌入到多线程的Web服务器(如Apache的worker MPM)中时,多个线程能否安全地共用一个PHP解释器实例。
  2. 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\LockChannel:推荐,只在协程级别阻塞。
$lock = new Swoole\Lock(Swoole\Lock::SPIN); // 自旋锁
$lock->lock();
// ... 操作共享数据,如读写一个数组 ...
$lock->unlock();
  • 文件锁:适用于跨进程的互斥。

方案三:利用协程的Channel

Channel可以看作一个协程安全的队列。你可以利用它来实现生产者-消费者模式,或者作为互斥令牌(一次只放一个令牌进去,谁拿到令牌谁执行)。

use Swoole\Coroutine\Channel;

$mutex = new Channel(1);
$mutex->push(true); // 初始化一个令牌

// 协程A
$mutex->pop(); // 取走令牌
// 执行临界区代码...
$mutex->push(true); // 归还令牌

方案四:避免共享状态——使用进程模型

这是最彻底、最推荐的方式。尽量不在Worker进程内共享可变数据。如果需要共享数据,可以:

  1. 使用外部存储:将共享数据放在RedisMySQLSwoole Table中。利用外部存储自身提供的原子操作(如Redis的INCR)来保证安全
  2. 使用Swoole TableSwoole\Table是一个基于共享内存的高性能、自带行锁的数据结构,可以在多个Worker进程间安全地共享数据。
$table = new Swoole\Table(1024);
$table->column('count', Swoole\Table::TYPE_INT);
$table->create();
$table->set('counter', ['count' => 0]);

// 在任何协程/进程中,对同一行的操作是互斥的
$table->incr('counter', 'count');

总结与最佳实践

场景是否有并发安全问题?如何保证安全?
PHP-FPM。每个请求独立,资源即用即毁。不需要额外处理。
Swoole。Worker进程常驻,全局/静态变量、连接池等被共享。1\. 首选无状态设计,共享数据存于Redis等外部组件。 2\. 次选:使用Swoole\\Table(跨进程)或Swoole\\Atomic/Lock(进程内)。 3\. 避免:直接操作global数组或对象属性而不加锁。

在设计Swoole服务时,可以把你的Worker进程想象成无状态的,所有需要持久化的数据都通过Redis或MySQL的原子命令来修改。这样,你的PHP业务代码本身几乎不需要处理锁,而是依赖外部存储的原子性来保证安全。这种方法最简单、最可靠,也最容易横向扩展。