PHP 怎么无锁结构

wen PHP项目 2

本文目录导读:

PHP 怎么无锁结构

  1. 核心思路:利用 atomic 或 CAS(比较交换)
  2. 纯 PHP 的“伪无锁”:基于共享内存 + flock
  3. 针对无共享内存的“无锁”设计
  4. 避开锁的终极方案:Atomic 文件写
  5. 总结建议
  6. 重点提醒

在 PHP 中实现无锁结构(Lock-Free Structure)确实是一个相对进阶的话题,首先需要明确一个核心事实:PHP 的单线程模型和请求生命周期决定了它天然适合无锁编程,因为每个请求是独立的,不存在多线程竞争问题。

如果你想在 CLI 长驻进程(如 pcntl 多进程)或使用 Swoole / Workerman 等多线程/协程环境下实现无锁,可以从以下几个层面来考虑:


核心思路:利用 atomic 或 CAS(比较交换)

所谓无锁,本质上是依赖 CPU 的原子指令(如 CAS),PHP 本身不直接提供 CAS,但可以通过扩展来实现:

  • 推荐扩展:swooleAtomic

    • Swoole 提供了 Swoole\Atomic,底层是 C 语言实现的原子操作。

    • 代码示例(更安全,推荐在 Swoole 环境下使用):

      $atomic = new Swoole\Atomic(0);
      // 例如实现一个无锁计数器(cli 模式)
      // 多进程时可以安全使用
      $result = $atomic->add();
  • 替代方案:parallel 扩展(>=7.2, 实验性质)

    • 提供了 parallel\Runtime,底层也支持共享对象,但同样需要依赖原语。

纯 PHP 的“伪无锁”:基于共享内存 + flock

如果你不想依赖 Swoole,想要纯 PHP 实现,通常做不到真正的 CAS,但如果你指的是 避免互斥锁(flock)导致的阻塞,可以采用 基于文件锁的乐观锁 思路:

  • 乐观锁实现(CAS 模拟):适用于数据库或文件存储。

    function updateBalance(int $userId, int $newBalance) {
        $cache = new Redis();
        $pipeline = $cache->multi();
        // 核心:利用 Redis 的 WATCH 实现 CAS
        $cache->watch('balance:' . $userId);
        $currentBalance = $cache->get('balance:' . $userId);
        if ($currentBalance === false || $newBalance < 0) {
            $cache->unwatch();
            return false; // 失败
        }
        // 尝试写入
        $result = $cache->multi()
            ->set('balance:' . $userId, $newBalance)
            ->exec();
        return $result !== false; // 如果返回值不是 false 则成功
    }

    这是经典的 WATCH/MULTI/EXEC 事务模式,提供了 CAS 语义。


针对无共享内存的“无锁”设计

PHP 最常见的“无锁”其实是 并发隔离,这比“无锁数据”更简单高效:

  • 按请求隔离:利用一个全局数组(如 $_SERVER 或自定义静态变量),然后在 register_shutdown_function 中统一落盘。
  • Actor 模型(如 Workerman + Channel):每个 Worker 进程内部使用自己的内存,进程间通过消息传递。消息传递消除锁竞争,这通常比共享内存锁更快。

Example (Workerman 消息通道):

$worker = new Workerman\Worker();
$worker->count = 4; // 多个进程
$channel = new Workerman\Channel\Client('127.0.0.1', 2206);
$worker->onMessage = function($conn, $data) use ($channel) {
    // 在这个闭包内,变量 $data 是本进程独有的,无需加锁
    // 如果要将数据发送给其他进程或服务,只需 $channel->push('event', $data);
};

避开锁的终极方案:Atomic 文件写

如果真的需要跨进程(如多个 CLI 脚本)写一个日志文件,可以使用 O_APPEND + file_put_contents 的原子特性(单行写入小于 4K 时 POSIX 保证原子性):

file_put_contents('/var/log/app.log', $logMessage, FILE_APPEND | LOCK_EX);

LOCK_EX 实际上是一个独占锁,但它在写入极短时性能损失可忽略,优于 flock 长期持有的阻塞锁。


总结建议

场景 推荐做法 是否真正无锁
高并发 Web 请求 使用 Redis / Nginx + APCu(如 apcu_fetch 做原子递增) 是(APCu 底层是 CAS)
Swoole 常驻进程 使用 Swoole\Atomic 是(原子操作)
跨流程(非常少见) System V 共享内存 (shmop) + sem_acquire 否(信号量相当于锁)
纯 PHP 不允许扩展 用数据库乐观锁(UPDATE ... WHERE 判断版本号) 语义上是 CAS

重点提醒

在 PHP 中,真正的“无锁”通常意味着“无阻塞”而非“无锁”,应用层的最佳实践是让数据尽量不要共享,而不是去实现复杂的无锁链表/哈希表(那是 C++/Rust 的领域),使用 Redis 这样的外部存储,把锁的粒度移动到网络层,已经是 PHP 场景下最常用的“伪无锁”方案。

如果你的场景是严格的性能敏感型(如百万并发计数器),建议走出 PHP,考虑使用 Nginx 的 ngx_http_redis 模块或直接用 Swoole 的原子计数,这两种都是底层 C 语言的原子操作。

抱歉,评论功能暂时关闭!