读书笔记
💉 《Operating System:Three Easy Pieces》第三十一章 信号量
00 分钟
2023-1-18
2023-4-15
type
status
date
slug
summary
tags
category
icon
password
Property
Apr 15, 2023 03:23 PM
作为与同步有关的所有工作的唯一原语。

信号量的定义

信号量是有一个整数值的对象,可以用两个函数来操作它。在 POSIX 标准中,是sem_wait()sem_post()。因为信号量的初始值能够决定其行为,所以首先要初始化信号量,才能调用其他函数与之交互。
其中申明了一个信号量 s,通过第三个参数,将它的值初始化为 1。sem_init()的第二个参数,在我们看到的所有例子中都设置为 0,表示信号量是在同一进程的多个线程共享的。 读者可以参考手册,了解信号量的其他用法(即如何用于跨不同进程的同步访问),这要求第二个参数用不同的值。 信号量初始化之后,我们可以调用 sem_wait()sem_post()与之交互。
  • sem_wait()要么立刻返回(调用 sem_wait()时,信号量的值大于等于 1),要么会让调用线程挂起,直到之后的一个 post 操作。当然,也可能多个调用线程都调用 sem_wait(),因此都在队列中等待被唤醒
  • sem_post()并没有等待某些条件满足。它直接增加信号量的值,如果有等待线程,唤醒其中一个
  • 当信号量的值为负数时,这个值就是等待线程的个数。虽然这个值通常不会暴露给信号量的使用者,但这个恒定的关系值得了解,可能有助于记住信号量的工作原理。

二值信号量(锁)

用信号量作为锁

信号量用作条件变量

信号量也可以用在一个线程暂停执行,等待某一条件成立的场景。
例如,一个线程要等待一个链表非空,然后才能删除一个元素。在这种场景下,通常一个线程等待条件成立,另外一个线程修改条件并发信号给等待线程,从而唤醒等待线程。因为等待线程在等待某些条件(condition)发生变化,所以我们将信号量作为条件变量(condition variable)。
一个线程创建另外一线程,并且等待它结束:

生产者/消费者(有界缓冲区)问题

读者—写者锁

另一个经典问题源于对更加灵活的锁定原语的渴望,它承认不同的数据结构访问可能需要不同类型的锁。
例如,一个并发链表有很多插入和查找操作。插入操作会修改链表的状态(因此传统的临界区有用),而查找操作只是读取该结构,只要没有进行插入操作,我们可以并发的执行多个查找操作。
读者之间不互斥,写者之间互斥,读者和写者互斥:
这一方案可行(符合预期),但有一些缺陷,尤其是公平性。读者很容易饿死写者。存在复杂一些的解决方案,有写者等待时,如何能够避免更多的读者进入并持有锁。 最后,应该指出,读者-写者锁还有一些注意点。它们通常加入了更多开锁(尤其是更复杂的实现),因此和其他一些简单快速的锁相比,读者写者锁在性能方面没有优势。

哲学家就餐问题

notion image
题的基本情况是(见图 31.10):假定有 5 位“哲学家”围着一个圆桌。每两位哲学家之间有一把餐叉(一共 5 把)。哲学家有时要思考一会,不需要餐叉;有时又要就餐。而一位哲学家只有同时拿到了左手边和右手边的两把餐叉,才能吃到东西。关于餐叉的竞争以及随之而来的同步问题,就是我们在并发编程中研究它的原因。

每个哲学家的基本循环:

关键的挑战就是如何实现 getforks()和 putforks()函数,保证没有死锁,没有哲学家饿死,并且并发度更高(尽可能让更多哲学家同时吃东西)。

查找餐叉的数量:

如果哲学家 p 希望用左手边的叉子,他们就调用 left(p)。类似地,右手边的叉子就用right(p)。模运算解决了最后一个哲学家(p = 4)右手边叉子的编号问题,就是餐叉 0。我们需要一些信号量来解决这个问题。假设需要 5 个,每个餐叉一个:sem_t forks[5]

有问题的解决方案

使用这个方案会形成死锁,每个人都拿着左手边的叉子就无法继续了(循环等待)

一种方案:破除依赖

选第 4 个人改为先获取右手的叉子,打破循环等待
还有其他一些类似的“著名”问题,比如吸烟者问题(cigarette smoker’s problem),理发师问题(sleeping barber problem)

如何实现信号量

小结

信号量是编写并发程序的强大而灵活的原语。有程序员会因为简单实用,只用信号量,不用锁和条件变量
信号量基于锁和条件变量,可以实现两者的功能
作为锁时,可以自动管理等待队列
作为条件变量时,免去了 while 判断是否需要等待的操作,因为内部包含了一个 value 值用于判断

参考


评论
Loading...