在使用多进程编程时,避免不了对资源的竞争。比如同时去更新数据库中的一条记录当中的amount(金额)字段,假设原始的值为0.00,此时进程A要把amount的值加100.00,而进程B要将amount的值加50.00,经过两次相加后,我们预期的amount值为150.00。
假设进程同时去操作,他们读到的amount都是0.00, 进程A 加100.00后为100.00,进程B加50.00后得到的是50.00,再假设进程A先把数据更新到数据库,此时数据库库中的amount是100.00,而后,进程B在更新记录,数据库中的amount会从100.00更新为50.00,这样就会造成和预期不一致的结果。

为了解决这个问题,我们需要有两种方式:

  • 使用锁
  • 保证其中一个先执行,一个后执行

使用锁的方式叫进程互斥,保证其中一个先执行就是让后执行的进程等待,先执行的进程执行完成后告诉等待的进程可以执行了,这个就叫进程间同步,这两种方式都可以认为是进程间的通信。

在unix家族中,进程间通信有管道、信号、消息队列和共享内存等几大类。

首先,我们使用php实现一个进程间的互斥锁。

php有一个 sem_*系列的函数,可以实现进程锁,sem_*系列函数实际操作的是 system V 信号,通过信号实现进程间的互斥锁。

获取一个信号资源

1
resource sem_get ( int $key [, int $max_acquire = 1 [, int $perm = 0666 [, int $auto_release = 1 ]]] )

参数说明:

  • key 一个整数,可以是任何值
  • max_acquire 整数,默认为1, 表示有多少个进程可以同时获得信号,进程锁需要设置为1
  • perm 权限 使用默认值
  • 是否自动释放资源,使用默认值

如果使用同一个key, 多次调用seg_get, 会返回不同的值, 但是返回的所有的值都指向同一个信号资源

使进程占据信号

1
bool sem_acquire ( resource $sem_identifier [, bool $nowait = false ] )

参数说明:

  • sem_identifier 由sem_get返回的资源
  • nowait 是否不等待进程获占据到信号资源后才返回,设置为false时,如果信号被占据,会一直等待其他进程释放信号后才返回;设置为true时,如果信号被占据,立即返回false; 大多数情况我们使用默认值。

释放对信号的占据

1
bool sem_release ( resource $sem_identifier )

参数说明:

  • sem_identifier 由sem_get返回的资源

释放信号资源

1
bool sem_release ( resource $sem_identifier )

参数说明:

  • sem_identifier 由sem_get返回的资源

现在我们写一个伪代码的例子,模拟多个进程去操作同一资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
$workCount = 10; // 进程数量
$workPids = array(); // 保存进程id
$semKey = 168;

$publicSource = 'some source';
$sem = sem_get($semKey);
if (!$sem) {
exit("seg get error" . PHP_EOL);
}

for ($id = 0; $i < 10; $i ++) {
$pid = pcntl_fork();
if (-1 == $pid) {
exit("fork error" . PHP_EOL);
} elseif (0 == $pid) {
// 子进程
// 实现加锁功能:如果信号被占据,不会立即返回,会等待其他进程释放以后才返回
$acquire = sem_acquire($sem, false);
if (!$acquire) { // 锁失败
exit("sem_acquire 失败" . PHP_EOL)
}
$publicSource->update(); // 更新公共资源
sem_release($sem); // 释放锁:更新完成后释放对信号的占据

} else {
$workPids[$pid] = $pid; // 保存子进程id
}

}
// 父进程
// 等待子进程全部操作完毕
$status = null;
while (cout($workPids) > 0) {
$pid = pcntl_wait($status); // 等待子进程退出,主进程会阻塞在这里,当有一个子进程退出时会返回改子进程id
unset($workPids[$pid]);
echo "child {$pid} exit" . PHP_EOL;
}
// 所有子进程运行完毕后释放信号资源
sem_remove($sem);

进程间使用共享内存同步

现在我们考虑一个场景,父进程fork一个子进程后,需要由父进程初始化一个资源,等待父进程初始化以后,子进程才能继续操作。由于进程的执行顺序是不确定的,所以我们不能保证子进程运行的时候父进程已经运行完成并且初始化好了资源,这需要由父进程告诉子进程–即进程间同步。

php的shm_*函数可以实现对共享内存的操作

创建共享内存块

1
resource shm_attach ( int $key [, int $memsize [, int $perm = 0666 ]] )

参数说明:

  • key 任意正整数 共享内存块的id
  • memsize 正整数 共享内存块的大小 单位是byte,如果不提供此参数,则检查php.ini中的配置项sysvshm.init_mem是否存在,存在使用配置的值,不存在则默认10000 bytes
  • perm 权限 使用默认值

函数返回一个共享内存块的资源标识

往共享内存里写入数据

1
bool shm_put_var ( resource $shm_identifier , int $variable_key , mixed $variable )

参数说明:

  • shm_identifier 由shm_attach返回的资源标识
  • variable_key 数据的标识
  • variable 存入的数据,支持能被函数serialize()序列化的所有值,例如资源或者一些内置的对象,一般情况下,如果是字符串、整形或者boolean这种简单的类型,我们可以直接存入,其他的数据可以使用json_encode或者serialize处理后在存入。

写入成功返回true, 否则返回false

获取共享内存中的数据

1
mixed shm_get_var ( resource $shm_identifier , int $variable_key )

参数说明:

  • shm_identifier 由shm_attach返回的资源标识
  • variable_key 数据的标识

返回存入的值

判断数据是否存在共享内存中

1
bool shm_has_var ( resource $shm_identifier , int $variable_key )

参数说明:

  • shm_identifier 由shm_attach返回的资源标识
  • variable_key 数据的标识

当数据存在于共享内存中时返回true, 否则返回false

销毁共享内存块

1
bool shm_detach ( resource $shm_identifier )

参数说明:

  • shm_identifier 由shm_attach返回的资源标识

shm_detach总是返回true

了解了这几个函数以后,我们来实开头提到的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
$shmKey = 8888; // 共享内存key
$shmSize = 1000; // 共享内存块大小
$initedKey = 'inited_key';

if (!$shm = shm_attach($shmKey, $shmKey)) {
exit("shm attach failed" . PHP_EOL);
}


$pid = pcntl_fork();
if (-1 == $pid) {
exit("fork error" . PHP_EOL);
} elseif (0 == $pid) {
// 子进程
while (true) { // 开始循环等待
if (!shm_has_var($shm, $initedKey)) { // 判断inited_key不存在于内存中
sleep(2); // 停两秒
continue; // 继续等待
}
// inited_key 存在于内存中
$inited = shm_get_var($shm, $initedKey); // 获取inited_key的值
echo "inited_key=" . $inited . PHP_EOL;
break;
}

}
// 父进程
// 资源初始化中.......................
// 初始化完成,告诉子进程
$tellChild = shm_put_var($shm, $inited, "初始化好了");
if (!$tellChild) {
exit("通知子进程(写入内存)失败,需要手动结束子进程" . PHP_EOL);
}
$status = null;
pcntl_waitpid($pid, $status); // 挂起主进程,等待子进程退出

shm_detach($shm); // 释放共享内存块

后面的话

在熟悉了互斥锁和进程间通信后,下篇博客中我们一起实现一个多进程的任务处理系统,这种处理方式已经运用在我司的数据迁移,统计和批量任务等方面