在 PHP 的多进程环境中(如使用 pcntl_fork 创建的子进程),单例模式的实现需要特别注意进程间共享和同步问题。由于 PHP 的多进程模型基于 fork,子进程会复制父进程的内存空间,因此单例实例在子进程中会被视为独立的副本,无法实现跨进程的“唯一性”。以下是具体的解决方案和注意事项:

1. 理解问题

单例模式在多进程中的局限性
  • 父进程和子进程的隔离性:每个进程拥有独立的内存空间,单例实例在子进程中会被复制,导致每个进程都有自己的实例。
  • 共享资源的冲突:如果单例实例管理的是共享资源(如数据库连接、文件句柄),多个进程的实例可能导致资源竞争或重复操作。
示例问题
class Singleton {
    private static $instance;
    private function __construct() {}
    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

// 父进程
$pid = pcntl_fork();
if ($pid == -1) {
    die("Fork failed");
} elseif ($pid) {
    // 父进程
    $s1 = Singleton::getInstance();
    echo "Parent: " . spl_object_id($s1) . "\n";
    pcntl_wait($status); // 等待子进程结束
} else {
    // 子进程
    $s2 = Singleton::getInstance();
    echo "Child: " . spl_object_id($s2) . "\n";
}

输出结果

Parent: 123456
Child: 789012
  • 每个进程的单例实例是独立的,spl_object_id 不同。

2. 解决方案

方案一:使用外部存储实现跨进程共享

通过将单例实例的状态存储在外部共享资源(如 Redis、文件、数据库)中,实现跨进程共享。

示例:使用 Redis 共享状态
class Singleton {
    private static $instance;
    private $redis;

    private function __construct() {
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1', 6379);
    }

    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function setKey($key, $value) {
        $this->redis->set($key, $value);
    }

    public function getKey($key) {
        return $this->redis->get($key);
    }
}

特点

  • 所有进程通过 Redis 访问共享数据,避免本地单例的隔离性问题。
  • 适用于需要跨进程共享状态的场景。
方案二:进程间同步(互斥锁)

通过锁机制(如文件锁、信号量)确保同一时间只有一个进程可以创建或修改单例实例。

示例:使用文件锁(flock
class Singleton {
    private static $instance;
    private static $lockFile;

    private function __construct() {}

    public static function getInstance() {
        $lockFile = fopen(__DIR__ . '/singleton.lock', 'w+');
        if (flock($lockFile, LOCK_EX)) { // 获取独占锁
            if (self::$instance === null) {
                self::$instance = new self();
            }
            flock($lockFile, LOCK_UN); // 释放锁
        } else {
            throw new Exception("Failed to acquire lock");
        }
        fclose($lockFile);
        return self::$instance;
    }
}

特点

  • 使用文件锁(flock)确保进程同步。
  • 适用于需要严格控制单例创建的场景。
  • 注意:文件锁在不同操作系统上的行为可能略有差异。
方案三:避免跨进程共享,独立管理

如果业务逻辑允许,可以让每个进程独立管理自己的单例实例,无需跨进程共享。

示例:独立管理
class ProcessSingleton {
    private static $instance;
    private function __construct() {}

    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

// 父进程和子进程各自独立管理
$pid = pcntl_fork();
if ($pid == -1) {
    die("Fork failed");
} elseif ($pid) {
    // 父进程
    $s1 = ProcessSingleton::getInstance();
    echo "Parent: " . spl_object_id($s1) . "\n";
} else {
    // 子进程
    $s2 = ProcessSingleton::getInstance();
    echo "Child: " . spl_object_id($s2) . "\n";
}

特点

  • 每个进程拥有独立的单例实例。
  • 适用于无需跨进程共享的场景(如日志记录、本地缓存)。

3. 高级方案:使用共享内存(System V 或 shmop)

通过共享内存(shmopsysvshm 扩展)实现进程间共享单例状态。

示例:使用 sysvshm
class SharedSingleton {
    private static $instance;
    private static $shmId;

    private function __construct() {}

    public static function getInstance() {
        $key = ftok(__FILE__, 'a');
        self::$shmId = shm_attach($key, 1024, 0666);

        if (self::$instance === null) {
            // 检查共享内存中是否已存在实例
            $data = shm_get_var(self::$shmId, 1);
            if ($data === false) {
                self::$instance = new self();
                shm_put_var(self::$shmId, 1, self::$instance);
            } else {
                self::$instance = $data;
            }
        }
        return self::$instance;
    }
}

特点

  • 通过共享内存实现进程间共享。
  • 需要安装 sysvshm 扩展,且需处理序列化/反序列化问题。

4. 总结

方案 适用场景 优点 缺点
外部存储(Redis/DB) 需要跨进程共享状态 简单易实现 依赖外部服务
文件锁/信号量 严格控制单例创建 轻量级 可能存在性能瓶颈
独立管理 无需跨进程共享 无需同步机制 实例不共享
共享内存 高性能共享 快速访问 配置复杂,需处理序列化

5. 选择建议

  • 如果需要跨进程共享状态:优先使用 Redis数据库
  • 如果需要严格同步控制:使用 文件锁信号量
  • 如果业务逻辑允许独立管理:直接使用普通单例,无需额外处理。

通过合理选择方案,可以在 PHP 多进程环境中灵活实现单例模式的需求。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐