信号量的原理:资源计数器 + PCB等待队列 + 函数接口
资源计数器:对共享资源的计数
当执行流获取信号量成功后,信号量当中的计数器减一,如果获取失败,该执行流就会被放入PCB等待队列中
当执行流释放信号量成功之后,信号量当中的计数器会进行加一操作
PCB等待队列:用于存放等待信号量的线程
函数接口:用于操作信号量的一组函数
信号量不仅仅可以完成线程之间的同步与互斥,也可以完成进程之间的同步与互斥
互斥原理
1、初始化信号量后,信号量当中的计数器保存的值为1,表示只有一个资源可以被使用
2、当执行流A想要访问共享资源时,首先获取信号量,此时计数器中的值为1,表示可以访问,执行流获取到信号量后,计数器的值从1变成0,执行流A此时去访问共享资源
3、此时,执行流B希望去访问共享资源,首先它要获取信号量,但是信号量中的计数器中的值为0,表示无法获取该信号量,进行无法访问共享资源,因为执行流B的PCB被放进了PCB等待队列中,等待目标信号量的释放,同时,信号量当中的计数器的值进行减一操作,计数器中的值变成了-1,这里的-1表示当前还有1个执行流在等待访问共享资源
同步原理
1、当执行流想要访问共享资源时,首先需要获取信号量
2、如果信号量中的计数器的值大于0,则表示能够获取信号量,进而可以访问共享资源
3、如果信号量中计数器的值小于或等于0,则表示不能获取信号量,进而无法访问共享资源,该执行流被放入PCB等待队列中,同时计数器进行减一操作
4、当释放信号量的时候,会对信号量中计数器进行加一操作
5、如果信号量中的计数器大于0,则唤醒PCB等待队列中的线程
6、如果信号量中的计数器小于或等于0,则不唤醒PCB等待队列中的线程
POSIX信号量的函数的名字都是以sem_
开头,常用的POSIX信号量函数有以下这些:
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, struct timespec*abs_timeout);
sem_t是信号量的类型(sem_t是一个结构体其中有资源计数器和PCB等待队列)
sem_t源码:
typedef union
{
char __size[__SIZEOF_SEM_T];
long int __align;
} sem_t;
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化一个信号量
参数:
数值 | 含义 |
---|---|
0 | 用于线程间,全局变量 |
非0 | 用于进程间,将信号量所用到的资源在共享内存当中进行开辟 |
value:资源的个数,本质上就是计数器的值
int sem_wait(sem_t *sem);
功能:
执行流调用该函数后,将会对计数器进行减一操作–p操作(P 操作用于获取或锁定信号量),(信号量的计数器自己保证原子性操作不会因为多线程而导致程序计数器中的结果二义性减一操作一步完成。)
如果减一操作后,计数器的值大于0,表示其他执行流仍然可以访问共享资源
如果减一操作后,计数器的值等于0,表示该执行流可以访问共享资源,其他执行流若想访问需要进入PCB等待队列
如果减一操作后,计数器的值小于0,表示当前执行流需要进入PCB等待队列,其他执行流若想访问也需要进入PCB等待队列
参数:sem:指向被操作的信号量,传入信号量的地址
要注意的是,先获取信号量再获取互斥锁,先获取信号量,再保证互斥,就是说,接口一定是先对程序计数器进行减一操作,再拿到锁,假设,如果先拿到锁,再进行信号量减一,那么当拿到锁之后,信号量如果从0减为小于0的数字,那么执行流就会被放到PCB等待队列中去了,这个函数也没有传输互斥锁,所以内部不会进行解锁,所以这时线程就会带着锁进行等待队列,然后无法解锁,锁一直被锁着,其他临界区也无法访问当前临界区资源
int sem_post(sem_t *sem);
功能:
执行流调用该函数后,将会对计数器进行加一操作–v操作(V 操作用于释放或解锁信号量)
判断资源计数器的值是否小于等于0
之所以还要判断是否等于0是因为,假设有一个生产者队列和一个消费者队列,当生产者将队列生产满了之后,假设此时程序计数器为-1,而将程序计数器减为-1的那个线程还在等待队列中,此时消费者线程被生产者唤醒,消费数据,出队,它将生产者信号量计数器中加一变成0,那么此时也要通知等待队列中的生产者线程出来工作
是:通知PCB等待队列
否:不用通知PCB等待队列,因为没有线程在等待
参数:sem:指向被操作的信号量,传入信号量的地址
int sem_destroy(sem_t *sem);
功能:销毁目标信号量
参数:sem:指向被操作的信号量,传入信号量的地址
代码如下:
#include
#include
#include
#include
#include
#define CAPACITY 4
#define THREAD_COUNT 2
int g_val = 0;
using namespace std;
class Safe_Queue{
public:
Safe_Queue(){
capacity_ = CAPACITY;
sem_init(&lock_, 0, 1);
//锁的信号量资源,要么为0表示不可用,要么为1表示可用
sem_init(&cons_sem_, 0, 0);
//消费者的信号量,最开始的时候,队列为空,所以消费者没有资源可用
sem_init(&prod_sem_, 0, capacity_);
//生产者的信号量,最开始的时候,队列为空,所以生产者可用资源数就是队列大小
}
~Safe_Queue(){
sem_destroy(&lock_);
sem_destroy(&prod_sem_);
sem_destroy(&cons_sem_);
}
//插入接口-生产者调用
void Push(int data){
sem_wait(&prod_sem_);//等待信号量,执行过后,对计数器进行减一操作
sem_wait(&lock_);//获取访问_que资源
_que.push(data);
printf("I am product, I product %d\n", data);
//sem_post(&cons_sem_);//对消费者可用的资源计数加一
sem_post(&lock_);//释放信号量函数,执行过后,对计数器进行加一操作
sem_post(&cons_sem_);//对消费者可用的资源计数加一
}
//获取元素接口-消费者调用
int Pop(){
sem_wait(&cons_sem_);
sem_wait(&lock_);
int temp = _que.front();
_que.pop();
printf("I am consume, I consume %d\n", temp);
//sem_post(&prod_sem_);
sem_post(&lock_);
sem_post(&prod_sem_);
return temp;
}
private:
//STL中的queue是线程不安全的,所以需要进行保护
queue<int> _que;
sem_t lock_;//用来保证队列资源互斥的信号量
sem_t prod_sem_;//生产者的信号量
sem_t cons_sem_;//消费者的信号量
size_t capacity_;//人为约定队列的大小
};
void* cons_start(void* arg){
Safe_Queue *q = (Safe_Queue*)arg;
while(1){
q->Pop();
}
return NULL;
}
void* prod_start(void* arg){
Safe_Queue *q = (Safe_Queue*)arg;
int data = 0;
while(1){
q->Push(data);
data++;
}
return NULL;
}
int main(){
Safe_Queue *q = new Safe_Queue();
if(q == NULL) return 0;
pthread_t cons[THREAD_COUNT], prod[THREAD_COUNT];
for(int i=0; i<THREAD_COUNT; ++i){
int ret = pthread_create(&prod[i], NULL, prod_start, (void*)q);
if(ret < 0){
perror("pthread_create");
return 0;
}
ret = pthread_create(&cons[i], NULL, cons_start, (void*)q);
if(ret < 0){
perror("pthread_create");
return 0;
}
}
for(int i=0; i<THREAD_COUNT; ++i){
pthread_join(cons[i], NULL);
pthread_join(prod[i], NULL);
}
return 0;
}
执行结果: