信号量

来看下百科的解释。

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。

信号量只有下面四种操作:

1.创建
2.请求(P)
3.释放(V)
4.删除

我们可以利用信号量来实现下面这些功能:

1.互斥操作(实现多进程/线程之间的同步)
2.限流

下面我将给小伙伴们分别演示一下。

互斥(同步)

线上代码:

<?php

// 信号量
$sem = sem_get(1, 1);

$pid = pcntl_fork();
if ($pid === -1) {
    exit('无法创建子进程');
} elseif ($pid > 0) {
    // 父进程
    $pid = getmypid();
    echo "父进程({$pid}):我开始运行了\n";
    while (1) {
        sleep(mt_rand(1, 5));
        $result = sem_acquire($sem);
        echo "父进程:当前时间". microtime(true),"\n";
        sem_release($sem);
    }
} else {
    // 这里是子进程
    $pid = getmypid();
    echo "子进程({$pid}):我开始运行了\n";
    while (1) {
        sleep(mt_rand(1, 5));
        $result = sem_acquire($sem);
        echo "子进程:当前时间". microtime(true),"\n";
        sem_release($sem);
    }
}

老规矩,先解释下代码。首先,我们调用 sem_get() 函数创建一个信号量,它的参数如下:

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

函数具体可看:https://www.php.net/manual/zh/function.sem-get.php

第一个参数,指定整形 key该值为0的话,返回的是一个 private semaphore,这个我们下文会说到它是什么。

第二个参数,max_acquire,默认值为1,我们可以利用它实现限流。也将在接下来说到。

其它参数不解释,看名字也好理解。

上述代码中,在父进程和子进程中,我们分别调用了 sem_acquire($sem) 这个函数来获取信号量,因为上面信号量的创建的时候 max_acquire 我们指定了 1 。也就意味着,同一时间,只有一个进程能够拿到信号。在拿到信号之后,输出一段话,就通过 sem_release($sem) 释放了这个信号。

我们来运行下:

父进程(18450):我开始运行了
子进程(18451):我开始运行了
父进程:当前时间1592105330.61
子进程:当前时间1592105333.6072
父进程:当前时间1592105334.6134
子进程:当前时间1592105336.6085
父进程:当前时间1592105339.6164
子进程:当前时间1592105340.6097
子进程:当前时间1592105341.6101
父进程:当前时间1592105341.6178

从结果中看到,父进程和子进程交互的获取到信号并执行。

限流

接下来,我们来看下信号量的限流写法:

<?php

// 信号量
$sem = sem_get(1, 4);

$i = 6;

while ($i-- > 0) {
    $pid = pcntl_fork();
    if ($pid === -1) {
        exit('无法创建子进程');
    } elseif ($pid > 0) {
        // 父进程
        if ($i === 0) {
            $pid = getmypid();
            echo "父进程({$pid}):我开始运行了\n";
            pcntl_wait($status);
        }
    } else {
        // 这里是子进程
        $pid = getmypid();
        echo "子进程{$i}({$pid}):我开始运行了\n";
        sleep(1);
        sem_acquire($sem);
        while (1) {
            echo "子进程{$i}:当前时间". microtime(true)."\n";
            sleep(3);
        }
    }
}

首先,我们创建了一个可以同时让4个进程拿到信号的信号量,也就是 $sem = sem_get(1, 4); 。接着我们用 while 循环,创建了6个子进程。子进程在等待1秒之后,开始尝试着获取信号量,拿到信号量之后就一直拿着不释放。我们来执行下:

子进程5(18688):我开始运行了
子进程4(18689):我开始运行了
子进程3(18690):我开始运行了
子进程2(18691):我开始运行了
父进程(18687):我开始运行了
子进程1(18692):我开始运行了
子进程0(18693):我开始运行了
子进程4:当前时间1592105699.2813
子进程5:当前时间1592105699.2813
子进程3:当前时间1592105699.2813
子进程2:当前时间1592105699.2813
子进程4:当前时间1592105702.2825
子进程2:当前时间1592105702.2826
子进程5:当前时间1592105702.2825
子进程3:当前时间1592105702.2825

从结果中可以看到,虽然我们创建了6个子进程,但是只有其中的2,3,4,5进程拿到了信号量,0和1号子进程因为信号量资源被抢完了,没办法获取到,也就陷入了阻塞的状态,这个时候,其它进程不释放信号(也就是V),它们将会一直陷入阻塞。

上面就是我们利用信号量实现的限流控制。

Private Semaphore

上面我们也提到了,如果将 sem_get 函数的第一个参数指定了0的话,将会返回一个 Private Semaphore 。那 Private Semaphore 是什么呢?有什么样的作用呢?

在了解它的作用之前,我们先来看下这个代码:

<?php

// 信号量
$sem = sem_get(1, 1);
$sem1 = sem_get(1, 1);
var_dump($sem, $sem1);

$pid = pcntl_fork();
if ($pid === -1) {
    exit('无法创建子进程');
} elseif ($pid > 0) {
    // 父进程
    $pid = getmypid();
    echo "父进程({$pid}):我开始运行了\n";
    sleep(1);
    sem_acquire($sem);
    while (1) {
        echo "父进程:当前时间". microtime(true)."\n";
        sleep(3);
    }
} else {
    // 这里是子进程
    $pid = getmypid();
    echo "子进程({$pid}):我开始运行了\n";
    sleep(1);
    sem_acquire($sem1);
    while (1) {
        echo "子进程:当前时间". microtime(true)."\n";
        sleep(3);
    }
}

上述代码中,我们调用了两次 sem_get() 函数,但是第一个参数 key 是一样的。我们来执行下:

resource(4) of type (sysvsem)
resource(5) of type (sysvsem)
父进程(19039):我开始运行了
子进程(19040):我开始运行了
子进程:当前时间1592106361.3346
子进程:当前时间1592106364.336
子进程:当前时间1592106367.3371
子进程:当前时间1592106370.3383

从var_dump打印的结果来看,$sem$sem1 的返回值是不一样的,但是从结果上看,子进程持续把持了信号量,父进程由于无法获取到信号量导致陷入阻塞。

我们再来看下这个代码:

<?php

// 信号量
$sem = sem_get(0, 1);
$sem1 = sem_get(0, 1);
var_dump($sem, $sem1);

$pid = pcntl_fork();
if ($pid === -1) {
    exit('无法创建子进程');
} elseif ($pid > 0) {
    // 父进程
    $pid = getmypid();
    echo "父进程({$pid}):我开始运行了\n";
    sleep(1);
    sem_acquire($sem);
    while (1) {
        echo "父进程:当前时间". microtime(true)."\n";
        sleep(3);
    }
} else {
    // 这里是子进程
    $pid = getmypid();
    echo "子进程({$pid}):我开始运行了\n";
    sleep(1);
    sem_acquire($sem1);
    while (1) {
        echo "子进程:当前时间". microtime(true)."\n";
        sleep(3);
    }
}

上述代码与前面的唯一不同就是 sem_get() 函数的第一个参数值变为了0。执行下:

resource(4) of type (sysvsem)
resource(5) of type (sysvsem)
父进程(19064):我开始运行了
子进程(19065):我开始运行了
子进程:当前时间1592106477.5335
父进程:当前时间1592106477.5365
子进程:当前时间1592106480.5349
父进程:当前时间1592106480.5394

var_dump 打印出来的结果,$sem$sem1 的返回值也是不一样的。但是父进程和子进程可以同时运行了,这意味着,它们的信号量请求并不是同一个!!!

到这里,小伙伴应该就明白了。sem_get() 第一个参数 key 如果大于0的话,相同 key 的情况下不管你调用几次,虽然返回在值不一样,但是在系统底层,它们对应的都是同一个信号量。

如果你将 key 的值指定为0的话,那么每次调用返回的都是一个全新的信号量。

文章目录