Linux环境中的semaphore使用
定义信号量
Linux下信号量的数据结构体为:sem_t。它本质上是一个长整型的数。
1 | sem_t firstSemaphore; |
利用sem_init()函数进行初始化
sem_init() 初始化一个定位在 sem 的匿名信号量。它的函数原型如下。
1 |
|
pshared 参数指明信号量是由进程内线程共享,还是由进程之间共享。如果 pshared 的值为 0,那么信号量将被进程内的线程共享,并且应该放置在这个进程的所有线程都可见的地址上(如全局变量,或者堆上动态分配的变量)。
value 参数指定信号量的初始值。
sem_init() 成功时返回 0;错误时,返回 -1,并把 errno 设置为合适的值。
信号量加减操作
函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不再阻塞,选择机制同样是由线程的调度策略决定的。sem_post() 成功时返回 0;错误时,信号量的值没有更改,-1被返回,并设置errno来指明错误。错误EINVAL sem不是一个有效的信号量。EOVERFLOW信号量允许的最大值将要被超过。
1 | int sem_post(sem_t *sem); // 信号量增加1 |
函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。
1 | int sem_wait(sem_t *sem); // 信号量减少1 |
函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。在成功完成之后会返回零。其他任何返回值都表示出现了错误。
1 | int sem_trywait(sem_t *sem); |
信号量其他操作
函数sem_destroy(sem_t *sem)用来释放信号量sem,属于无名信号量。在清理信号量的时候如果还有线程在等待它,用户就会收到一个错误。与其它的函数一样,这些函数在成功时都返回0。
1 | int sem_destroy(sem_t *sem); |
函数sem_getvalue(sem_t *sem)用来获取当前信号量的值。
1 | int sem_getvalue(sem_t *sem); |
线程同步实例
我们提供了一个类:
public class Foo {
public void first() { print(“first”); }
public void second() { print(“second”); }
public void third() { print(“third”); }
}
三个不同的线程 A、B、C 将会共用一个 Foo 实例。
一个将会调用 first() 方法
一个将会调用 second() 方法
还有一个将会调用 third() 方法
请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。
示例 1:
输入: [1,2,3]
输出: “firstsecondthird”
解释:
有三个线程会被异步启动。
输入 [1,2,3] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 second() 方法,线程 C 将会调用 third() 方法。
正确的输出是 “firstsecondthird”。
示例 2:
输入: [1,3,2]
输出: “firstsecondthird”
解释:
输入 [1,3,2] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 third() 方法,线程 C 将会调用 second() 方法。
正确的输出是 “firstsecondthird”。
1 |
|
在C++中自己实现semaphore
标准C++中没有实现自己的信号量机制,需要借助外部库或者自己实现。
下面的代码使用std::unique_lock和std::mutex实现了信号量机制。
1 |
|
经典的哲学家就餐问题
五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在桌子上有五只碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐毕,放下筷子继续思考。
哲学家从 0 到 4 按 顺时针 编号。请实现函数 void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork):
- philosopher 哲学家的编号。
- pickLeftFork 和 pickRightFork 表示拿起左边或右边的叉子。
- eat 表示吃面。
- putLeftFork 和 putRightFork 表示放下左边或右边的叉子。
- 由于哲学家不是在吃面就是在想着啥时候吃面,所以思考这个方法没有对应的回调。
三种策略实现:
1 |
|
使用条件变量condition_variable
condition_variable介绍
在C++11中,我们可以使用条件变量(condition_variable)实现多个线程间的同步操作;当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。
成员函数如下:
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
- 一个线程因等待”条件变量的条件成立”而挂起;
- 另外一个线程使”条件成立”,给出信号,从而唤醒被等待的线程。
在条件变量中只能使用std::unique_lock<std::mutex>
wait/wait_for函数
1 | void wait( std::unique_lock<std::mutex>& lock ); |
wait 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,可选地循环直至满足某谓词。
1 | template< class Rep, class Period > |
wait_for 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,或者超时返回。
由于系统存在虚假唤醒的情况,一般这样使用wait()函数:
1 | while (!pred()){ //while循环,解决了虚假唤醒的问题 |
该方法和下述方法等价。
1 | wait(lock,pred()); |
notify_all/notify_one
1 | void notify_one() noexcept; |
使用条件变量完成同步实例
我们提供一个类:
class FooBar {
public void foo() {
for (int i = 0; i < n; i++) {
print(“foo”);
}
}
public void bar() {
for (int i = 0; i < n; i++) {
print(“bar”);
}
}
}
两个不同的线程将会共用一个 FooBar 实例。其中一个线程将会调用 foo() 方法,另一个线程将会调用 bar() 方法。
请设计修改程序,以确保 “foobar” 被输出 n 次。
示例 1:
输入: n = 1
输出: “foobar”
解释: 这里有两个线程被异步启动。其中一个调用 foo() 方法, 另一个调用 bar() 方法,”foobar” 将被输出一次。
示例 2:
输入: n = 2
输出: “foobarfoobar”
解释: “foobar” 将被输出两次。
1 | class FooBar { |