Linux线程同步方法之三
信号量
信号量本质上是一个计数器,用来表示公共资源中资源的数量。只要拥有信号量,未来一定能够拥有临界资源的一部分使用权。故申请信号量的本质就是对临界资源中特定的小块资源的预订机制。故能通过访问信号量就能提前知道临界资源的使用情况。申请信号量成功,说明条件肯定满足,可以直接push/pop;申请信号量不成功,说明条件不满足<==>就不用再进行判断了
让所有线程在访问公共资源前,必须先申请sem信号量,只有成功了,才能进去–>前提是所有进程必须先看到同一个信号量–>信号量本身就是公共资源–>而信号量要保护其他资源,首先是保护自己,要保证自身操作的安全性–>信号量的++和–操作是原子的。
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的, 但POSIX可以用于线程间同步。
PV原语
- P操作
sem--;
预定资源 passeren - V操作
sem++;
释放资源 vrijgeven
函数
sem_init
#include <semaphore.h>
功能:初始化信号量
原型:
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared: 0表示线程间共享,非零表示进程间共享。一般为0
value: 信号量初始值
sem_destroy
int sem_destroy(sem_t *sem);
sem_wait
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
sem_post
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
上一节生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序(POSIX信号量)。
基于环形队列的单生产单消费模式代码实现
在数据结构阶段,了解过环形队列的判空和判满的方法(计数器或者空一个位置)。可以参考循环队列的解题思路
现在我们要用多线程来实现基于环形队列的单生产单消费。由于一开始没有数据,生产线程和消费线程一开始肯定是指向同一个位置的;当队列满了的时候,也会访问同一个位置;其余情况下两者不可能访问同一个位置。
在环形队列中,大部分情况下,单生产单消费可以并发执行,满/空的时候才有互斥与同步问题!
规则:1、当队列为空时,生产线程先跑;2、当队列为满时,消费线程先跑;3、消费线程不能无效访问;4、生产线程<消费线程+队列大小。
信号量是用来衡量临界资源的数量。对于生产者producer而言,是环形队列中剩余的空位置–可以定义一个信号量producer_sem = 1;
,要先申请信号量P(producer_sem);
,申请成功就往队列里放入数据,接着V(consumer_sem);
相当于让消费者知道自己所看重的资源数量增加,申请失败执行流阻塞;对消费者consumer而言,是环形队列中的已有数据个数–可以再定义一个信号量consumer_sem = 0;
,先申请信号量资源P(consumer_sem);
,申请成功取出数据后执行V(producer_sem);
,申请失败则阻塞等待。
生产者和消费者的位置其实就是队列中的下标,两者分别有一个下标,仅队列为空或者满的时候下标相同,要注意下标是否越界。
现象描述:当生产者比较慢,消费者比较快时,稳定时,生产一个新数据,消费一个新数据,队列一直为空或有1个数据;当生产者比较快,消费者比较慢时,稳定时,生产一个新数据,消费一个旧数据,队列一直为满或空1个位置。
基于环形队列的多生产者和多消费者模式
就需要加两把锁,多个生产者竞争一把锁,多个消费者竞争一个下标。
**应该先申请信号量,再加锁。**首先:不用保护信号量(因为信号量是原子操作);其次:可以在申请锁之前,线程就并发申请锁,效率更高,且并不妨碍我们要维护单一角色之间的互斥关系。这说明了可以提前把信号量申请完。
基于此,最终能进入临界区的最多的时候就是1个生产者、一个消费者(2个线程),最少的情况是1个生产者/消费者(1个线程)。
多生产多消费的意义是为了在放数据和取数据之前能做到多线程并发,取数据和放数据是串行执行的是为了保证数据安全。
最后的代码可以实现多生产多消费,且自行修改了一个用两个环形队列,完成生产、消费和保存的功能。(和条件变量类似)
线程池
线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。与内存池同理
线程池的应用场景:1. 需要大量的线程来完成任务,且完成任务的时间比较短;2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求;3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。