计数与等待是一体的

信号量是协调并发执行的同步对象,而不只是一个供程序随意读写的整数。可把其计数理解为当前可领取的许可:一次成功等待取得一个许可,一次发布提供一个许可。检查是否有许可与取得许可必须按同步操作完成,不能拆成未经保护的普通判断和减法。

在 Linux 的 POSIX 接口说明中,可用计数不小于零;计数为零时 sem_wait 等待,直到能够减一。部分教材用负数记录等待者人数,那是另一种描述模型,不应把它与接口展示的许可计数混同。

参考:[1] [2] [3]

初始值决定同步用途

若要保护一个临界区,可把许可数初始化为一,所有进入者先等待,离开时发布一次,并维持严格配对。这样同一时刻只有一个成功取得许可但尚未归还的执行者。这是二元使用方式;通用计数信号量并不会因为初值为一,就自动阻止错误的多次发布。

计数信号量也可以从多个许可开始,限制同时占用某类资源的操作数量。若目标是线程排序,则可从零开始:工作线程完成某阶段后发布,依赖它的线程等待。发布先发生时,许可仍保存在计数中;等待先发生时,等待者暂停,随后由发布使其能够继续。

有界缓冲区展示了许可和数据保护为何要分开:empty从容量开始,full从零开始,分别控制可写空位和可读项目;多个生产者或消费者操作缓冲区索引时,还需要互斥。正确次序是在等到相应许可后再取得互斥保护。若消费者先持有互斥锁,再因full为零而阻塞,生产者就可能无法取得同一把锁来放入数据。单个信号量的操作都正确,组合次序仍会造成死锁。

参考:[1] [2] [5]

与互斥锁的不同

二元信号量能在正确协议下提供互斥,但用途不限于锁。排序例子中,等待与发布由不同线程完成;互斥锁则有取得者这一持有者概念,释放行为受互斥量类型及持有关系约束。因此不能只因二者常出现“一进一出”就把接口任意替换。

互斥锁还有递归、错误检查和健壮性等特定语义。选择同步对象时,应先区分是在保护共享状态、计算资源许可,还是通知某个阶段完成。计数本身不自动保护其他共享数据,相关数据访问仍须遵循完整的同步协议。

参考:[4] [1]

成功等待与异常返回

POSIX 还提供非阻塞尝试和限时等待。Linux 的 sem_trywait 在不能立即取得许可时返回 EAGAIN;sem_timedwait 可以因超时返回。sem_wait 也可能被信号处理打断,因此调用返回并不必然等于成功取得许可,应检查结果再执行依赖该许可的操作。

信号量可用于线程之间,也可用于进程之间。POSIX 区分具名与无名形式;跨进程的无名信号量需要放在相应共享内存中。初始化、共享方式及生命周期属于使用协议的一部分,不能只复制一对等待和发布调用,就假定同步已经成立。

参考:[2] [3]