2022-2-19
- 条件变量
-
- 生产者/消费者(有界缓冲区)问题
- 最终的生产者/消费者方案
- 信号量
-
- 信号量的定义
- 二值信号量(锁)
- 信号量用作条件变量
- 生产者/消费者(有界缓冲区)问题
-
- 第一次尝试
- 第二次尝试
- 读写锁
- 哲学家就餐问题
-
- 一个简单的尝试
- 解决方法
条件变量
线程可以使用条件变量(condition variable),来等待一个条件变成真。条件变量是一个显式队列,当某些执行状态(即条件,condition)不满足时,线程可以把自己加入队列,等待(waiting)该条件。另外某个线程,当它改变了上述状态时,就可以唤醒一个或者多个等待线程(通过在该条件上发信号),让它们继续执行。
其中,pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m)的作用是,释放获得的锁m,并且获得c的旧值,与新值比较是否变化,没变化就进入休眠,变化了就对m进行加锁
注意这里的done变量是必要的,如果没有他来完成thr_join里的循环的话,pthread_cond_wait只会运行一次,如果检测到c没有完成的话,就解锁然后无限进行等待,无法被唤醒了
child运行完后,调用thr_exit,改变了条件变量c,解锁m后直接返回,这时thr_join就可以被唤醒继续运行了
另外需要注意的是,pthread_cond_wait默认使用者在休眠之前是持有锁的
生产者/消费者(有界缓冲区)问题
假设有一个或多个生产者线程和一个或多个消费者线程。生产者把生成的数据项放入缓冲区;消费者从缓冲区取走数据项,以某种方式消费。因为有界缓冲区是共享资源,所以我们必须通过同步机制来访问它,以免产生竞态条件。为了更好地理解这个问题,我们来看一些实际的代码。
这里可能出现一个问题,就是如果存在两个消费者T1和T2,在最开始他们先运行了,发现count == 0也即没数据的时候,都进入了休眠,然后生产者C被唤醒,存入一个数据,唤醒T1后睡眠,T1读取数据后,count又变成0,唤醒T2后进入睡眠,T2醒来后发现count ==0,于是又进入睡眠,这时大家都进入睡眠了
解决方法很简单,设置两个条件变量,决定哪种情况该唤醒哪种程序就行了
最终的生产者/消费者方案
上文的方案,虽然没问题,但是存一个数据就切换一次线程,读一个数据就切换一次线程,上下文切换开销太大,我们可以一次生产多个值,一次消费多个值,减少切换损耗
信号量
信号量的定义
信号量是有一个整数值的对象,可以用两个函数来操作它。在 POSIX 标准中,是sem_wait()和 sem_post()。因为信号量的初始值能够决定其行为,所以首先要初始化信号量,才能调用其他函数与之交互
其中申明了一个信号量 s,通过第三个参数,将它的值初始化为 1。sem_init()的第二个参数,在我们看到的所有例子中都设置为 0,表示信号量是在同一进程的多个线程共享的。
二值信号量(锁)
信号量的第一种用法就是当锁用
这里x应该为1
为了说明清楚,我们假设有两个线程的场景。第一个线程(线程 0)调用了 sem_wait(),它把信号量的值减为 0。然后,它只会在值小于 0 时等待。因为值是 0,调用线程从函数返回并继续,线程 0 现在可以自由进入临界区。线程 0 在临界区中,如果没有其他线程尝试获取锁,当它调用 sem_post()时,会将信号量重置为 1(因为没有等待线程,不会唤醒其他线程)
信号量用作条件变量
例如,一个线程要等待一个链表非空,然后才能删除一个元素。在这种场景下,通常一个线程等待条件成立,另外一个线程修改条件并发信号给等待线程,从而唤醒等待线程。
这里的初始值应该为0
生产者/消费者(有界缓冲区)问题
第一次尝试
这里有个问题,如果存在两个生产者p1和p2,p1运行,假设fill这时=0,p1调用put,给buffer赋值后,但还没给fill+1时,产生了中断,p2运行,也调用put,p2就将buffer[0]的值给覆盖了,这是因为临界区没加锁保护导致的两个线程发生竞争
第二次尝试
这里我们加入互斥锁,但又产生了一个问题,假如消费者先运行,mutex从1变0,然后full从0变为-1,消费者进入睡眠,生产者活动,但这时锁仍然是消费者持有的,所以生产者将mutex从0变为-1,进入睡眠,但是这时full仍然是-1,无法唤醒消费者,两个线程都进入睡眠,死锁了
解决方法也很简单,就是将两个锁的位置调换下
这告诉我们了,在上锁的时候,将锁紧挨着临界区才是最好的
读写锁
哲学家就餐问题
假定有 5 位“哲学家”围着一个圆桌。每两位哲学家之间有一把餐叉(一共 5 把)。哲学家有时要思考一会,不需要餐叉;有时又要就餐。而一位哲学家只有同时拿到了左手边和右手边的两把餐叉,才能吃到东西。关于餐叉的竞争以及随之而来的同步问题,就是我们在并发编程中研究它的原因。
下面是每个哲学家的基本循环:
while(1){
think();getforks();eat();putforks();
}
由于有五个叉子,所以我们需要五个信号量:sem_t forks[5]
一个简单的尝试
这里的问题是,如果每个哲学家都拿起了左手的叉子,就死锁了,大家都在等待右手边的叉子被释放
解决方法
解决方法也很简单,让最后一位哲学家先拿右手边的叉子就行