linux --线程(四)posix信号量

信号量介绍

之前我们在进程间通信中也讲到过system V 信号量–>《信号量及pv操作》
先列出用于同步的三种信号量
1.System V信号量(在内核中维护):可用于进程或线程间的同步。
2.Posix有名信号量(在内核中维护):使用PosixIPC名字标识(通过特定函数,调用一个绝对文件路径名作为参数,返回一个特定标识),可用于进程或线程间通信。
3.Posix无名信号量(基于内存的信号量):存放在共享内存区(进程间共享内存区或者线程间共享内存区),可用于进程或线程间同步。

对于posix信号量的有名和无名
区别:
1.创建和销毁方式不同
2.基于不同的单元维护,
相同:
具体操作都是信号量集机制实现的

无名信号量只存在于内存中,并且规定能够访问该内存的进程才能够使用该内存中的信号量.所以无名信号量只能被这样两种线程使用:
(1)来自同一进程的各个线程
(2)来自不同进程的各个线程,
但是这些进程映射了相同的内存范围到自己的地址空间。
相反,有名信号量则是通过名字访问,任何进程只要知道该有名信号量的名字都可以访问。
总结就是:
有名信号量:其值保存在文件中,因此可以用于进程间的同步。
无名信号量:其值保存在内存中,因此常用于线程间的同步。

posix信号量

这篇文章我们所介绍的posix信号量为无名信号量,来对同一进程中的各个线程进行同步!

功能
主要完成主要完成线程间或者进程间的同步与互斥
本质
资源计数器+ PCB等待队列+提供等待和唤醒接口

对比与条件变量
之前我们也学过条件变量来用于线程间同步与互斥《线程同步之条件变量》,那对比条件变量来说,posix信号量多了一个资源计数器,这个资源计数器用来对临界资源进行计数。信号量通过判断自身的资源计数器,来进行条件判断(而条件变量中需要程序猿自己通过while循环来判断)。判断后的逻辑都是一样的:
(1)判断资源可用:则获取资源进行访问(2)判断资源不可用:则进行阻塞等待,直到被唤醒.

操作接口
定义

sem_ t  
eg : sem_t sem;
来自 /usr/include/bits/semaphore.h
sem_t是一个联合体,用于定义信号量
typedef union
{
  char __size[__SIZEOF_SEM_T];  //__SIZEOF_SEM_T宏定义为16或32
  long int __align;
} sem_t;

初始化

#include <semaphore.h>  //信号量接口所存在的头文件
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
	sem :传入信号量的地址
	pshared:0表示线程间共享,非零表示进程间共享
	value:信号量初始值, 即实际资源数量,用于初始化信号量当中资源计数器的


当使用sem_ init初始化信号量为进程间的时候,会在内核当中创建一块共享内存,来保存信号量的数据结构,其中资源计数器、PCB等待队列都是在共享内存当中维护的。所以我们调用唤醒或者等待接口的时候,就通过操作共享内存实现了不同进程之间的通信。进而实现不同进程之间的同步与互斥的。
注意

  • 如果我们要在两个进程之间使用该信号量,我们需要确保sem参数指向这两个进程共享的内存范围内(即在定义时要把这个sem参数(信号量)设置成全局变量或者类私有成员)。

    扫描二维码关注公众号,回复: 11161018 查看本文章
  • 因为二值信号量可以像互斥量(mutex)一样使用,我们可以使用信号量创建我们自己的锁原语(primitive)来提供互斥。

等待信号量
功能:等待信号量,获取信号量后会将信号量的值减1
p操作 -1.

若成功则返回0,出错则返回-1
----阻塞方式的等待
int sem_wait(sem_t *sem);

---非阻塞方式的等待
int sem_trywait(sem_t* sem);

---带有超时时间的等待
int sem_timedwait(sem_t* sem, const struct timespec* timeval)

如果信号量计数为0,这时如果调用sem_wait函数,将会阻塞。直到成功完成对信号量计数减1或被一个信号中断,sem_wait函数才会返回。我们可以使用sem_trywait函数以避免阻塞。当我们调用sem_trywait函数时,如果信号量计数为0,sem_trywait会立即返回-1,并将errno设置为EAGAIN。

唤醒(发布)信号量
功能:发布信号量,表示资源使用完成了,需要归还资源或者生产者重新生产了一个资源,对信号量进行加1操作。唤醒PCB等待队列当中等待该信号量值变为正数的PCB
V操作 +1.

int sem_post(sem_t *sem);

当我们调用sem_pos完成后,如果此时有因为调用sem_wait或sem_timedwait而阻塞的进程,那么该进程将被唤醒,并且刚刚被sem_post加1的信号量计数紧接着又被sem_wait或sem_timedwait减1。

销毁信号量

int sem_destroy(sem_t *sem);

获取信号量值

 int sem_getvalue(sem_t *sem, int *sval);

通过sval获得当前信号量值,返回值要么是0,要么是此时阻塞在这个信号量上的进程或者线程数。

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

由于我们只能对POSIX信号量的值进行加1或减1。对信号量值减1,就类似于对一个二值信号量(信号量值只能为0/1)加锁或请求一个与计数信号量(非负整数)相关的资源。
对信号值加1,对应解锁或唤醒等待队列中的线程使用资源。
注意,POSIX信号量并没有区分信号量类型。使用二值信号量还是计数信号量,取决于我们如何对信号进行初始化和使用。

实现互斥:如果信号量值只能取0和1,那么它就是一个二值信号量。当一个二值信号量值为1,我们则说它未加锁;若它的值为0,则说它已加锁。
实现同步:根据我们初始化指定的资源数量来初始化posix信号量中的资源计数器,判断计数器(计数信号量)的值,为正数说明可以获取资源,减一操作,为0时,表示暂时没有资源可用,阻塞等待,直到有资源时被唤醒。

基于环形队列的生产消费模型
实现一个循环队列的数据结构来存储资源
由于队列中队头和队尾的位置都是动态变化的,因此需要附设两个指针PosWrite_ 和PosRead_ ,分别指示队头元素和队尾元素在数组中的位置。
在这里插入图片描述

#include <iostream>
#include <unistd.h>
#include <semaphore.h>
#include <pthread.h>
#include <vector>

#define SIZE 1
#define THREADCOUNT 1

class RingQueue
{
    public:
        RingQueue()
            :Vec_(SIZE)
        {
            CapaCity_ = SIZE;//队列容量初始化
            PosWrite_ = 0;//读写位置初始化
            PosRead_ = 0;
				
            //使用信号量用于生产者的同步,初始化策略是:信号量计数器的值和数组的空闲空间一样大
            sem_init(&ProSem_, 0, SIZE);
            //使用信号量用于消费者的同步,初始化的策略是:在刚开始的时候,由于数组还没有一个有效元素,所有初始化资源数为0,后边生产线程在唤醒消费线程的时候,会对消费者信号量当中的计数器进行加加操作,从而消费者线程可以获取到消费信号量,进而去访问资源
            sem_init(&ConSem_, 0, 0);

            //使用信号量用于互斥锁操作,初始化的策略是,初始化资源数为1,即只有一个线程在同一时刻能够拥有该信号量
            sem_init(&Lock_, 0, 1);
        }

        ~RingQueue()
        {
         //销毁信号量
            sem_destroy(&ProSem_);
            sem_destroy(&ConSem_);
            sem_destroy(&Lock_);
        }

        void Push(int& Data)
        {
            sem_wait(&ProSem_);//生产者获取信号量,成功了,从此函数返回,继续执行下面的逻辑,没有就阻塞等待 
            //P操作 计数器减一

            sem_wait(&Lock_); //加锁
            Vec_[PosWrite_] = Data;//生产到队列
            PosWrite_ = (PosWrite_ + 1) % CapaCity_;//循环队列更新下标
            sem_post(&Lock_);//解锁

            sem_post(&ConSem_);//通知消费者
        }

        void Pop(int* Data)
        {
            sem_wait(&ConSem_);//消费者获取信号量,成功了,就返回,然后顺序执行下面的逻辑,没有就阻塞等待

            sem_wait(&Lock_);//加锁
            *Data = Vec_[PosRead_];//从队列里消费,出参
            PosRead_ = (PosRead_ + 1) % CapaCity_;///更新下标
            sem_post(&Lock_);//解锁

            sem_post(&ProSem_);//通知生产者
        }
    private:
        std::vector<int> Vec_;//数组实现循环队列
        size_t CapaCity_; //容量

        //读写位置
        int PosWrite_;//生产者写入数据
        int PosRead_;//消费者读出数据

        //信号量实现同步
        sem_t ProSem_;
        sem_t ConSem_;

        //信号量实现互斥
        sem_t Lock_;
};


void* ConsumeStart(void* arg)//消费者读取数据的逻辑
{
    RingQueue* rq = (RingQueue*)arg;
    int Data;
    while(1)
    {
        rq->Pop(&Data);//出参
        printf("ConsumeStart [%p][%d]\n", pthread_self(), Data);
    }
    return NULL;
}

void* ProductStart(void* arg)//生产者写入数据的逻辑
{
    RingQueue* rq = (RingQueue*)arg;
    int i = 0;
    while(1)
    {
        rq->Push(i);
        printf("ProductStart [%p][%d]\n", pthread_self(), i);
        i++;
    }
    return NULL;
}

int main()
{
    RingQueue* rq = new RingQueue();

    pthread_t com_tid[THREADCOUNT], pro_tid[THREADCOUNT];//定义消费者生产者线程

    int i = 0;
    for(; i < THREADCOUNT; i++)
    {
        int ret = pthread_create(&com_tid[i], NULL, ConsumeStart, (void*)rq);
        if(ret < 0)
        {
            printf("create thread failed\n");
            return 0;
        }

        ret = pthread_create(&pro_tid[i], NULL, ProductStart, (void*)rq);
        if(ret < 0)
        {
            printf("create thread failed\n");
            return 0;
        }
    }

    for(i = 0; i < THREADCOUNT; i++)
    {
        pthread_join(com_tid[i], NULL);//回收线程资源
        pthread_join(pro_tid[i], NULL);
    }

    delete rq; //释放申请的空间
    rq = NULL;
    return 0;
}

信号量与互斥锁之间的区别

  1. 互斥量用于线程的互斥,信号量用于线程的同步。
    (1)信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程在进行某些动作(阻塞或者执行)。
    (2)互斥锁是用在多线程多任务互斥的,一个线程加锁占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。
    这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
    • 互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
    • 同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源(读写锁模式)。
  1. 互斥量值只能为0/1,信号量值可以为非负整数。
    也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为二值信号量时,也可以完成一个资源的互斥访问(即信号量也可以用来实现互斥量的功能)。
  2. 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
原创文章 59 获赞 170 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_44785014/article/details/105634790