【Linux】多线程 之 信号量

信号量

信号量的同步与互斥实现

作用:实现线程或进程之间的同步与互斥
本质:计数器+等待队列+等待与唤醒的功能接口
场景:用于实现进程/线程间的同步与互斥
信号量实现同步:通过自身的计数器进行资源计数,对临界资源访问之前先访问信号量,通过计数判断是否有资源能够访问,若不能访问( 计数<=0 没有资源),则等待,并且计数-1,(如果没有资源了,资源为0,等待并且计数-1,变成-1),当信号量计数器为负值,就说明有多少线程正在等待;若可以访问-计数>0,则计数-1,直接访问;其它线程生产资源促使条件满足后,则判断若计数>0,计数+1(表示没人等待,只把计数+1就可以了,说明此时已经有资源了),否则若计数<0,则唤醒一个等待队列上的线程,并计数+1.(<0说明有人在等待,计数+1,就说明等待的人少了一个)
信号量实现互斥
只需要将计数维持在0/1之间就可以实现互斥

信号量与条件变量实现同步的区别:

条件变量实现同步,没有提供给用户条件判断的功能,是需要用户自己来判断的而信号量本身有计数器,资源计数,有没有资源都是通过计数器来进行资源计数的,通过计数器,我们就能知道有没有资源,能不能访问,跟信号量最大的区别就是条件变量通过自身的计数来实现条件判断
信号量通过自身条件判断实现同步,条件变量是通过用户进行条件判断;并且条件变量使用的是外部用户的条件判断,必须是搭配互斥锁一起使用,而信号量,是通过自身的条件判断,在内部保证了操作的原子性。

信号量的接口函数:

1. 定义信号量: sem_t类型   (sem_t类型的结构体中肯定有 计数器,阻塞队列,等待与唤醒的接口)


2. 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
信号量变量,(sem_t 结构体至少要有计数器,等待队列,保证计数安全内部有其它的锁)
pshared :标志位,决定了当前的信号量用与进程间还是线程间,0-线程间 ,!0-进程间
(因为是一个库,线程间用全局变量就可以完成,用于进程间(进程间相互独立),实际上申请了一块共享内存,在共享内存中包含了计数器	和共享队列)
信号量的初值(由用户根据资源而定)如果定义的是互斥锁的话,那么初始值就是1,因为互斥是由0和1控制的。
成功0  失败-1


4. 在访问临界资源之前,先访问信号量,判断是否访问: 计数-1
条件判断(有没有资源自给判断)+等待
 int sem_wait(sem_t *sem) ;//若计数<= 0, 则-1后阻塞;否则-1后立即返回  --阻塞函数
  trywait(sem_t *sem)-- 通过自身计数判断是否满足访问条件,不满足则立即报错返回 EINVAL
timedwait(sem_t *sem,const struct timespec* abs_timeout)-- 限制时长的阻塞,超时后报错返回 ETIMEDOUT


5. 促使访问条件满足 + 1, 唤醒阻塞线程/进程
int sem_post(sem_t *sem);	//唤醒信号量等待队列上的线程 且计数+1


6. 销毁信号量
int sem_destroy(sem_t * sem); //销毁信号量

注意: 一个信号量只有一个等待队列,所以我们用信号量实现生产者与消费者模型的时候,需要定义两个信号量实现同步,每一个信号量需要等待在每一个不同的等待队列上。

信号量如何实现同步与互斥

信号量实现同步

厨师做饭的例子:

 sem_t _sem;
 void * food()
 {
    int i = 0;
    while(1)
 	{
      sem_wait(&_sem); // 没有资源则阻塞等待,有资源则-1访问资源
      
      printf("%d delicious ~~\n",i++);    
  	}
 
    return NULL;
 }
 void* cook()
 {
     while(1)
    {
       printf("cook ~~\n");
       sleep(1);
       sem_post(&_sem);  //资源+1                                                                                                       
    }
     return NULL;
   }
int main()
{
	sem_init(&_sem,0,0); //一开始没有资源,因为还没有做饭,所以没有资源,第三个参数为0
	。。。
}

信号量实现互斥

抢车票的例子:(伪代码)

int tickets = 100;
sem_t _sem;

void* buy(void* arg)
{
	while(1)
	{
		sem_wait(&_sem); //P操作, -1加锁, 现在初始资源为0,代表加锁状态
		if(tickets > 0)
		{
			tickets--;
			sem_post(&_sem); //V操作,+1,解锁, 唤醒等待队列上的线程
		}
		else
		{
			return NULL;
		}
	}
	return NULL;
}

int main()
{
	sem_init(&_sem,0,1); //第二个参数0是线程间,因为信号量实现互斥是0/1判断是否有资源,所以第三个参数资源初始值为1
	int  ret = pthread_create(&buyer,NULL,buy,NULL);
         ret = pthread_create(&seller,NULL,buy,NULL);
         pthread_join(buyer,NULL);
         pthread_join(buyer,NULL);
         sem_destroy(&_sem);

}

使用信号量实现生产者与消费者模型(代码实现)

使用信号量实现一个线程安全的阻塞队列,模拟实现生产者与消费者模型

信号量实现生产者与消费者模型的思想
我们使用数组来实现环形的阻塞队列,通过数据空间和剩余空间的大小代表消费者和生产者线程,生产者表示现在有多少剩余空间没被使用,消费者是对数据空间的计数。

使用三个信号量实现环形阻塞队列,1个信号量实现互斥,一个用于对空闲空间进行计数 , 实现入队操作的合理性,一个用于对数据进行计数 , 实现出队操作的合理性。

注意点:

  1. 加锁保护的是临界资源
  2. 在访问是否有剩余空间或者剩余数据的时候,要先判断是否有资源,再去加锁。如果加锁了,然后再去判断是否有空闲空间,若没有则阻塞,但是这里的阻塞和条件变量的阻塞不一样,不会解锁的。你阻塞了,别人就都阻塞了
  3. 要先解锁再去唤醒消费者或者生产者线程,因为锁只保护临界资源
    1 #include <iostream>                                                                                                              
    2 #include <vector>
    3 #include <pthread.h>
    4 #include <semaphore.h>
    5 using namespace std;
    6 
    7 #define MAX_QUE 10
    8 
    9 
   10 class RingQueue
   11 {
   12     //三个信号量实现环形阻塞队列
   13     //1个信号量实现互斥
   14     //一个用于对空闲空间进行计数 -- 实现入队操作的合理性
   15     //一个用于对数据进行计数 -- 实现出队操作的合理性
   16 private:
   17     vector<int> _queue; //使用数组实现环形队列
   18     int _capacity; // 初始化环形队列的节点数量
   19     int _step_read; // 当前读位置的数组下标
   20     int _step_write; // 当前写位置的数组下标
   21     sem_t _sem_lock; // 用户实现互斥的锁,信号量实现互斥的锁。
   22     sem_t _sem_con;  //消费者等待队列,计数器对有数据的空间进行计数
   23     sem_t _sem_pro; // 生产者等待队列,计数器对有空闲的空间进行计数
   24 public:
   25     RingQueue(int max_que = MAX_QUE)
   26         :_capacity(max_que)
   27          ,_queue(max_que)
   28          ,_step_read(0)
   29          ,_step_write(0)
   30     {
   31         //初始化信号量
   32         sem_init(&_sem_lock, 0, 1);// 第二个参数0是用在线程间的,第三个参数的1是 条件变量实现的互斥锁不能大于1 只能0或1,初始值1>
   33         sem_init(&_sem_con,0,0);
   34         sem_init(&_sem_pro,0,max_que); //生产者,初始化是节点的数量,数组有多少空节点,
   35         // 就可以访问多少次,生产多少数据
   36 
   37     }
   38     ~RingQueue()
   39     {
   40         //销毁信号量
   41         //
   42         sem_destroy(&_sem_lock);
   43         sem_destroy(&_sem_con);
   44         sem_destroy(&_sem_pro);
   45     }
   46 
   47     bool QueuePush(int &data)
   48     {
   49 
   50         //lock和_sem_lock的顺序不能变
   51         //如果加锁了,然后再去判断是否有空闲空间,若没有则阻塞,
   52         //但是这里的阻塞和条件变量的阻塞不一样,不会解锁的。你阻塞了,别人就都阻塞了
   53         //所以是先判断是否有资源访问,能够访问了才加锁, 加锁保护的只是临界资源的访问,
   54         //
   55         //
   56         //sem_post(&_lock)
   57         //信号量实现互斥:保证计数不大于1             
```58         //对资源访问之前先sem_wait(&lock),判断计数是否>0,大于0,才会访问,并计数-1;,否则会阻塞
   59         //通过0和1的计数实现互斥,访问的时候-1,计数变为0,别人就不能访问,而阻塞
   60         //我访问完了之后就得解锁,
   61         //就是再把计数器+1,唤醒别的线程也能访问,这就是sem_post
   62     
   63         //数据入队
   64         //1. 判断是否能够访问临界资源。判断空闲空间计数是否大于0,判断是否有空闲空间 
   65         sem_wait(&_sem_pro);//如果小于0.生产者等待在它的等待队列桑
   66         //2. 先加锁
   67         sem_wait(&_sem_lock);// lock的计数大大于1,当前若可以访问则-1
   68         
   69         //3. 数据的入队操作
   70         _queue[_step_write] = data;
   71         _step_write = (_step_write + 1)% _capacity;
   72         //4. 解锁
   73         sem_post(&_sem_lock); // lock计数+1 此时lock为1,其他线程 可以访问互斥锁
   74         //5. 数据资源的空间计数+1, 唤醒消费者
   75         sem_post(&_sem_con); 
   76 
   77 
   78         return true;
   79     }
   80     bool QueuePop(int & data)
   81     {
   82         sem_wait(&_sem_pro); // 看是否有数据
   83         sem_wait(&_sem_lock);// 有数据则加锁保护访问数据的过程
   84 
   85         data = _queue[_step_read]; // 获取数据
   86         _step_read = (_step_read+1)%_capacity;
   87      
   88         sem_post(&_sem_lock);// 解锁操作
   89         sem_post(&_sem_pro); //取出数据,并且对空闲计数+1,唤醒消费者
   90 
   91         return true;
   92     }
   93 };
   94
    

猜你喜欢

转载自blog.csdn.net/weixin_43939593/article/details/106516821