PHP `Shared Memory` (`shm_`) / `System V IPC` (`sem_`, `msg_`):多进程数据共享与同步

各位观众老爷们,晚上好!今天咱们聊点硬核的——PHP多进程之间的数据共享与同步。这玩意儿听起来高大上,其实也就那么回事儿,就好像你跟隔壁老王共享WiFi密码一样,只不过对象从一个人变成了一堆进程,共享的东西从密码变成了一堆数据,仅此而已。

咱们今天要讲的主要是两大家族:Shared Memory (也就是 shm_ 函数) 和 System V IPC (包括 sem_msg_ 函数)。这两大家族都是操作系统提供的利器,PHP只是给它们套了个壳,方便咱们用。

一、Shared Memory (共享内存) – 一块大家都能摸的黑板

想象一下,你和一群朋友在玩拼图,拼图的碎片散落在桌子上,每个人都可以拿到碎片,然后拼到一块大黑板上。这个黑板就是共享内存。

  • 优点: 速度快!因为所有进程直接访问同一块物理内存,不需要数据拷贝。
  • 缺点: 需要自己负责同步,不然拼图就乱了,大家吵起来了。

1.1 shm_get() – 申请黑板

这个函数的作用是获取一个共享内存块的ID。如果这个ID对应的内存块不存在,它就会创建一个新的。

<?php

$key = ftok(__FILE__, 't'); // 生成一个唯一的key,ftok根据文件路径和项目ID生成key
$shmid = shm_get_var($key, 1);
if($shmid === false){
    $shmid = shm_attach($key, 1024, 0666); //1024 字节大小,0666 权限
    if ($shmid === false) {
        die("无法创建共享内存!n");
    }
}
echo "共享内存ID: " . $shmid . "n";

?>
  • ftok(): 这个函数很关键,它根据文件路径和项目ID生成一个唯一的key,这个key用来标识共享内存。不同的进程只要使用相同的key,就能访问同一块共享内存。 ftok(__FILE__, 't') 通常的做法,用当前文件路径和项目ID ‘t’ 作为参数。
  • shm_attach(): 创建共享内存块,返回共享内存ID。如果已经存在,则返回已存在的ID。
    • 第一个参数是key,也就是 ftok() 生成的key。
    • 第二个参数是大小,单位是字节。
    • 第三个参数是权限,0666表示所有用户可读写。

1.2 shm_put_var() 和 shm_get_var() – 在黑板上写字和擦字

这两个函数用于在共享内存中存储和读取数据。

<?php

$key = ftok(__FILE__, 't');
$shmid = shm_attach($key, 1024, 0666);

if ($shmid === false) {
    die("无法连接到共享内存!n");
}

$data = ['name' => '张三', 'age' => 30];

if (shm_put_var($shmid, 1, $data)) {
    echo "数据写入成功!n";
} else {
    echo "数据写入失败!n";
}

$retrieved_data = shm_get_var($shmid, 1);

if ($retrieved_data !== false) {
    print_r($retrieved_data);
} else {
    echo "数据读取失败!n";
}

?>
  • shm_put_var():
    • 第一个参数是共享内存ID。
    • 第二个参数是变量ID,相当于黑板上的编号,不同的变量ID对应不同的存储位置。
    • 第三个参数是要存储的数据。
  • shm_get_var():
    • 第一个参数是共享内存ID。
    • 第二个参数是变量ID。

1.3 shm_remove() 和 shm_detach() – 擦黑板和离开教室

  • shm_remove(): 从系统中移除共享内存块。注意,这并不会立即释放内存,而是标记为待删除,当所有进程都detach之后,才会真正释放。谨慎使用! 如果你删除了共享内存,其他进程再来访问就懵逼了。
  • shm_detach(): 将进程从共享内存块分离。就好比下课了,你离开了教室,不再使用这块黑板。
<?php

$key = ftok(__FILE__, 't');
$shmid = shm_attach($key, 1024, 0666);

if ($shmid === false) {
    die("无法连接到共享内存!n");
}

// ... (你的代码) ...

if (shm_detach($shmid)) {
    echo "成功从共享内存分离!n";
} else {
    echo "从共享内存分离失败!n";
}

// 只有当没有进程attach到共享内存的时候,shm_remove才能真正释放内存
// 通常情况下,shm_remove函数应该由专门的进程来调用
// 比如一个守护进程,它负责创建和销毁共享内存
// 避免多个进程同时调用shm_remove导致的问题
// if (shm_remove($shmid)) {
//     echo "成功移除共享内存!n";
// } else {
//     echo "移除共享内存失败!n";
// }

?>

1.4 共享内存的同步问题 – 黑板上的秩序

共享内存最大的问题就是同步。多个进程同时读写同一块内存,如果没有同步机制,就会出现数据混乱,就像一群熊孩子在黑板上乱涂乱画一样。

常用的同步机制有:

  • 信号量 (Semaphores): 就像交通信号灯,控制对共享资源的访问。 一次只允许一个进程访问,避免冲突。
  • 互斥锁 (Mutexes): 和信号量类似,也是保证互斥访问。

咱们先讲信号量,互斥锁也差不多。

二、System V IPC – 信号灯,消息队列,一个都不能少

System V IPC 是一套进程间通信的机制,包括信号量、消息队列和共享内存(虽然共享内存也算,但咱们上面已经单独讲过了)。

2.1 信号量 (Semaphores) – 红绿灯指挥交通

信号量就像交通信号灯,控制对共享资源的访问。 一次只允许一个进程访问,避免冲突。

  • sem_get() – 申请一个信号灯
<?php

$key = ftok(__FILE__, 's'); // 生成一个唯一的key
$sem_id = sem_get($key, 1, 0666, 1); // 1个信号量,0666权限,初始值为1

if ($sem_id === false) {
    die("无法创建信号量!n");
}

echo "信号量ID: " . $sem_id . "n";

?>
  • sem_get():

    • 第一个参数是key,和共享内存一样,用 ftok() 生成。
    • 第二个参数是信号量集合中信号量的数量,通常是1,表示只有一个信号量。
    • 第三个参数是权限。
    • 第四个参数是初始值。 1 表示资源可用,0 表示资源不可用。
  • sem_acquire() – 红灯停,绿灯行

<?php

$key = ftok(__FILE__, 's');
$sem_id = sem_get($key, 1);

if ($sem_id === false) {
    die("无法获取信号量!n");
}

if (sem_acquire($sem_id)) {
    echo "成功获取信号量!n";
    // ... (访问共享资源的代码) ...
    sleep(5); // 模拟耗时操作
    echo "操作完成!n";
    if(sem_release($sem_id)){
        echo "释放信号量成功";
    }else{
        echo "释放信号量失败";
    }

} else {
    echo "获取信号量失败!n";
}

?>
  • sem_acquire(): 尝试获取信号量。如果信号量的值大于0,则减1,并继续执行。如果信号量的值等于0,则阻塞,直到其他进程释放信号量。

  • sem_release(): 释放信号量,将信号量的值加1,唤醒等待中的进程。

  • sem_release() – 绿灯亮,放行

<?php

$key = ftok(__FILE__, 's');
$sem_id = sem_get($key, 1);

if ($sem_id === false) {
    die("无法获取信号量!n");
}

if (sem_release($sem_id)) {
    echo "成功释放信号量!n";
} else {
    echo "释放信号量失败!n";
}

?>
  • sem_remove() – 拆除红绿灯
<?php

$key = ftok(__FILE__, 's');
$sem_id = sem_get($key, 1);

if ($sem_id === false) {
    die("无法获取信号量!n");
}

if (sem_remove($sem_id)) {
    echo "成功移除信号量!n";
} else {
    echo "移除信号量失败!n";
}

?>

2.2 消息队列 (Message Queues) – 邮局送信

消息队列就像邮局,进程可以将消息发送到队列中,其他进程可以从队列中接收消息。

  • msg_get_queue() – 建立邮局
<?php

$key = ftok(__FILE__, 'm'); // 生成一个唯一的key
$queue_id = msg_get_queue($key, 0666); // 0666 权限

if ($queue_id === false) {
    die("无法创建消息队列!n");
}

echo "消息队列ID: " . $queue_id . "n";

?>
  • msg_get_queue():

    • 第一个参数是key,和共享内存一样,用 ftok() 生成。
    • 第二个参数是权限。
  • msg_send() – 寄信

<?php

$key = ftok(__FILE__, 'm');
$queue_id = msg_get_queue($key);

if ($queue_id === false) {
    die("无法获取消息队列!n");
}

$msgtype = 1; // 消息类型
$message = ['name' => '李四', 'age' => 25];

if (msg_send($queue_id, $msgtype, $message)) {
    echo "消息发送成功!n";
} else {
    echo "消息发送失败!n";
}

?>
  • msg_send():

    • 第一个参数是消息队列ID。
    • 第二个参数是消息类型,必须是正整数。 消息类型可以用来过滤消息,接收者可以只接收特定类型的消息。
    • 第三个参数是要发送的消息。
  • msg_receive() – 收信

<?php

$key = ftok(__FILE__, 'm');
$queue_id = msg_get_queue($key);

if ($queue_id === false) {
    die("无法获取消息队列!n");
}

$msgtype = 0; // 接收所有类型的消息
$received_message = null;

if (msg_receive($queue_id, 0, $msgtype, 1024, $received_message)) { // 1024 是最大接收长度
    echo "消息接收成功!n";
    print_r($received_message);
} else {
    echo "消息接收失败!n";
}

?>
  • msg_receive():

    • 第一个参数是消息队列ID。
    • 第二个参数是要接收的消息类型。 0 表示接收所有类型的消息。
    • 第三个参数是接收到的消息类型,会被函数修改。
    • 第四个参数是最大接收长度。
    • 第五个参数是接收到的消息,会被函数修改。
  • msg_remove_queue() – 拆除邮局

<?php

$key = ftok(__FILE__, 'm');
$queue_id = msg_get_queue($key);

if ($queue_id === false) {
    die("无法获取消息队列!n");
}

if (msg_remove_queue($queue_id)) {
    echo "成功移除消息队列!n";
} else {
    echo "移除消息队列失败!n";
}

?>

三、实战演练 – 抢红包游戏

咱们来做一个简单的抢红包游戏,用共享内存存储红包余额,用信号量保证只有一个进程能抢到红包。

<?php

// 配置文件
define('SHM_KEY', ftok(__FILE__, 'r'));
define('SEM_KEY', ftok(__FILE__, 's'));
define('SHM_SIZE', 1024);
define('TOTAL_MONEY', 100); // 红包总金额

// 初始化
function init() {
    global $shm_id, $sem_id;

    // 创建共享内存
    $shm_id = shm_attach(SHM_KEY, SHM_SIZE, 0666);
    if (!$shm_id) {
        die("创建共享内存失败!n");
    }

    // 创建信号量
    $sem_id = sem_get(SEM_KEY, 1, 0666, 1);
    if (!$sem_id) {
        die("创建信号量失败!n");
    }

    // 初始化红包金额
    if (!shm_put_var($shm_id, 1, TOTAL_MONEY)) {
        die("初始化红包金额失败!n");
    }
}

// 抢红包
function rob_red_packet() {
    global $shm_id, $sem_id;

    // 获取信号量
    if (!sem_acquire($sem_id)) {
        echo "抢红包失败,请稍后再试!n";
        return false;
    }

    // 获取红包余额
    $money = shm_get_var($shm_id, 1);
    if ($money <= 0) {
        echo "红包已经被抢光了!n";
        sem_release($sem_id); // 释放信号量
        return false;
    }

    // 随机抢到的金额
    $rob_money = rand(1, $money);

    // 更新红包余额
    $money -= $rob_money;
    if (!shm_put_var($shm_id, 1, $money)) {
        echo "更新红包余额失败!n";
        sem_release($sem_id); // 释放信号量
        return false;
    }

    echo "恭喜你抢到 " . $rob_money . " 元,剩余 " . $money . " 元!n";

    // 释放信号量
    sem_release($sem_id);

    return true;
}

// 销毁
function destroy() {
    global $shm_id, $sem_id;

    // 分离共享内存
    shm_detach($shm_id);

    // 移除信号量 (谨慎使用)
    sem_remove($sem_id);

    // 移除共享内存 (谨慎使用)
    shm_remove($shm_id);
}

// 主程序
init();

// 模拟多个进程抢红包
$pid = pcntl_fork();

if ($pid == -1) {
    die("fork failed!n");
} else if ($pid) {
    // 父进程
    rob_red_packet();
    pcntl_wait($status); // 等待子进程结束
} else {
    // 子进程
    rob_red_packet();
    exit(0);
}

//destroy(); // 演示完成,销毁资源 (建议注释掉,方便测试)

?>

这个例子演示了如何使用共享内存和信号量来实现多进程之间的数据共享和同步。 你可以多开几个终端,同时运行这个脚本,模拟多个进程抢红包。

四、总结 – 工具箱里的宝贝

工具 作用 优点 缺点 适用场景
Shared Memory 共享内存块,多个进程可以直接读写同一块内存。 速度快! 需要自己负责同步,容易出现数据混乱。 需要频繁读写的大量数据,例如缓存。
Semaphores 信号量,控制对共享资源的访问。 可以实现互斥访问,避免数据冲突。 性能相对较低。 保护共享资源,例如数据库连接、文件句柄。
Message Queues 消息队列,进程可以将消息发送到队列中,其他进程可以从队列中接收消息。 可以实现异步通信,解耦进程之间的依赖关系。 性能相对较低,消息传递有延迟。 任务队列、事件通知。

总而言之,Shared MemorySystem V IPC 是PHP多进程编程中不可或缺的工具。 掌握它们,你就能写出更高效、更稳定的多进程应用。 当然,使用这些工具的时候一定要小心谨慎,做好同步,不然就容易出问题。

好了,今天的讲座就到这里,谢谢大家! 散会,散会! 记得点赞,收藏,加关注哦! 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注