Linux基础内容(28)—— POSIX信号量与循环队列

Linux基础内容(27)—— 线程同步与生产消费者模型_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/131809437?spm=1001.2014.3001.5501

目录

1.问题引入

2.信号量

1.本质

2.PV原语

3.基本结构使用

初始化

销毁

等待

发布

3.环形队列

1.理论

特性

理论实现(基于单消费者生产者)

总结

2.模拟

总结


1.问题引入

生产的过程有什么问题吗?

void push(const T &in)
{
    pthread_mutex_lock(&_mutex);
    while(is_full())
    {
        pthread_cond_wait(&_pcond,&_mutex);
    }
    _q.push(in);

    pthread_cond_signal(&_ccond);
    pthread_mutex_unlock(&_mutex);
}

我们加锁是为了让公共资源受到保护。但是只要加锁,直到解锁这之中的所有代码都被受到保护,那么也就默认了整体代码都被保护。但是这一部分资源有些地方是允许同时访问的。但是真正进入访问的其实是那些条件判断成功的,但是为了判断条件需要进入锁中这个步骤似乎有点低效率

2.信号量

1.本质

1.信号量本质其实是一把计数器

2.只要拥有信号量,就拥有访问部分临界资源的权利,那么人为的分配不同线程的访问公共资源不同区域也使得申请信号量的本质为对特定的小块资源进行预定

2.PV原语

1.信号量的计数器类型为sem_t

2.当我们设置类型和对应预约的大小以后,那么就只需要对进行PV操作;P为申请资源,对应的计数器就要减少,V为释放资源,对应的计数器需要增加

3.增加和减少的操作不能被其他线程所影响,那么信号量就需要保证操作的原子性

3.基本结构使用

初始化

Linux基础内容(28)—— POSIX信号量与循环队列_第1张图片

pshared:0为线程间共享,非0进程间共享

value:信号量初始值

销毁

Linux基础内容(28)—— POSIX信号量与循环队列_第2张图片

等待

Linux基础内容(28)—— POSIX信号量与循环队列_第3张图片

申请资源,也就是P操作,对信号量减少操作

发布

Linux基础内容(28)—— POSIX信号量与循环队列_第4张图片

释放资源,也就是V操作,对信号量增加操作

3.环形队列

1.理论

循环队列有很多的状态,这里其实不需要当头尾指针不相同时,说明此时的队列中既有空间也有数据,这种大部分的情况其实不需要去纠结。不过我们需要注意的是头和尾重合的情况,因为针对生产者和消费者,其实他们所需要的资源其实分别是:有空间存放任务和有数据处理。

特性

1.那么其实我们只需要管着头尾指针相同时,相同只有两种情况:1.环形队列满了 2.环形队列空着。

2.那么此时我们就能创造两个信号量分别给生产者和消费者,生产者检查环形队列是否为满,消费者检查环形队列是否为空。

3.并且顺序执行是先让生产者生产任务,消费者不得超过生产者。当处理时消费者和生产者不能同时访问

4.生产者和消费者同步互斥:同步(先后运行的顺序,以及信号量引入);互斥(不得同时访问)

理论实现(基于单消费者生产者)

1.首先由于消费者和生产者互斥,那么其实需要两个信号量,消费者的信号量表示是否有数据开始设定为空;生产者的信号量表示是否有空间设定为满。

2.消费者部分代码:开始我们先判断消费者信号量是否为空,那么就有两种情况

一、若为不满,则执行下面的代码进行消费生产者生产的任务,那么执行完生产者的任务后,消费者本身不能消除掉生产者的任务,所以需要调用生产者信号量将消费的资源释放掉

二、若为满,则说明此时数据为空,挂起等待

3.生产者部分代码:开始我们先判断生产者信号量是否为空,那么也有两种情况

一、若为不空,则执行下面的代码进行生产的任务,那么生产任务后,队列中一定有数据需要被消费者执行的,所以接下来调用消费者信号量对共享资源进行操作

二、若为空,则说明此时剩余的空间为空,挂起等待

总结

1.其实这样完全符合我们所需要的特性,当队列为满,那么此时生产者就不会工作;当队列为空,则消费者就不会工作。

2.那么最开始设置就是数据为空,自然消费者是不会执行的,空间为满,生产者自然可以生产数据,也就意味着生产者一定是先开始的。

3.一环逻辑中生产完就消费和消费完就生产完全满足同步的实现。那么在绝大部分情况下,消费者和生产者都在并发的实现

2.模拟

#define gcap 100

template 
class RingQueue
{
private:
    void P(sem_t &sem)
    {
        int n = sem_wait(&sem);
        assert(n == 0);
        (void)n;
    }

    void V(sem_t &sem)
    {
        int n = sem_post(&sem);
        assert(n == 0);
        (void)n;
    }

public:
    RingQueue(const int &cap = gcap)
        : _queue(cap), _cap(cap)
    {
        int n = sem_init(&_spaceSem, 0, cap);
        assert(n == 0);
        n = sem_init(&_dataSem, 0, 0);
        assert(n == 0);

        _productorStep = _consumerStep = 0;

        pthread_mutex_init(&_pmutex,nullptr);
        pthread_mutex_init(&_cmutex,nullptr);
    }

    void push(const T &in)
    {
        P(_spaceSem);
        pthread_mutex_lock(&_pmutex);
        _queue[_productorStep++] = in;
        _productorStep %= _cap;
        pthread_mutex_unlock(&_pmutex);
        V(_dataSem);
    }

    void pop(T *out)
    {
        P(_dataSem);
        pthread_mutex_lock(&_cmutex);
        *out = _queue[_consumerStep++];
        _consumerStep %= _cap;
        pthread_mutex_unlock(&_cmutex);
        V(_spaceSem);
    }

    ~RingQueue()
    {
        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);

        pthread_mutex_destroy(&_pmutex);
        pthread_mutex_destroy(&_cmutex);
    }

private:
    std::vector _queue;
    int _cap;
    sem_t _spaceSem;
    sem_t _dataSem;
    int _productorStep;
    int _consumerStep;
    pthread_mutex_t _pmutex;
    pthread_mutex_t _cmutex;
};

总结

1.push和pop的实现,使得生产者生产任务与消费者消费任务完全隔开。也就意味着二者之间互斥同步。

2.而push和pop具体的代码,加锁,使得所有的相同线程无法同时进入,使得多生产之间互斥,多消费之间也互斥。

3.至于信号量的判断要先于加锁的意义在于:我们如果加锁后判断其实会让其他线程无法操作判断,只能让拿到锁的线程进行操作。但是每一个线程必须要经过判断,那么处理信号量就意味着线程预约到了空间,那么不会对其他线程的空间造成影响,因为其就拥有一空间。所以这样减少了时间的成本。

4.多生产和多消费的意义:使得生产者和消费者能并发的访问,生产者生产任务和消费者处理任务不会相互影响,并且这两个环节的时间成本也高。

你可能感兴趣的:(Linux和操作系统,开发语言,c++,linux,服务器,centos)