【Linux庖丁解牛】— 信号量 !

1. POSIX信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。但 POSIX可以用于线程间同步。

信号量本质是一个计数器,是对特定目标资源的预定机制。

多线程使用资源其实有两种场景:

> 将目标资源整体使用。【比如我们基于阻塞队列设计的生产者消费者模型就是将整个阻塞队列上锁,也就是将目标资源整体使用。这可以用互斥锁实现】

> 将目标资源分块使用。【可以用信号量办到】

使用信号量来维护资源的安全要考虑两点:

1. 不要让多个线程访问同一块资源。

2. 不要放入过多的线程进来

2. 基于环形队列的生产者消费者模型

在环形队列中我们先做如下约定:

  1. 队列为空,生产者先运行。

  2. 队列为满,消费者先运行。

  3. 生产者不能将消费者套一个圈以上。

  4. 消费者不能超过生产者。

基于以上的约定,我们可以得出以下结论

  1. 只要在环形队列中,我们不访问同一个位置,我们就可以同时运行。

  2. 只有队列中为空或者为满的时候,生产者和消费者才可能在同一个位置。

  3. 如果在同一位置就只能互斥,为空则生产者先运行。为满,则消费者先运行。

但是,我们怎么保证以上的4个约定呢??

答案就是用信号量来保证!!下面是使用信号量保证约定的伪代码流程:

对于生产者来说,资源就是每一个空位置,我们用sem_blank信号量来表示。而对于消费者来说,资源就是数据,我们用sem_data来表示 。PV操作本质上就是在申请信号量,这是原子性的!当位置为0时,生产者就会在P操作上阻塞,直到位置不为0【 2. 队列为满,消费者先运行3. 生产者不能将消费者套一个圈以上**】。同理,当数据为0时,消费者就会在P操作上阻塞,直到数据不为0【** 1. 队列为空,生产者先运行4. 消费者不能超过生产者**】!当队列不为空,也不为满时,它们就可以同时操作**!

3. 信号量接口

认识接口:

  1. 初始化信号量:pshared【0表示线程共享,非零表示线程间共享】,value为信号量的初始值。
  1. 销毁信号量
  1. 信号量P操作
  1. 信号量V操作

封装信号量:

sem.hpp

复制代码
#pragma once

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

namespace sem_module
{
    const unsigned int default_val = 5;
    class sem
    {
    public:
        sem(unsigned int val = default_val)
        {
            sem_init(&_sem, 0, val);
        }

        // P操作
        void P()
        {
            sem_wait(&_sem);
        }

        // V操作
        void v()
        {
            sem_post(&_sem);
        }

        ~sem()
        {
            sem_destroy(&_sem);
        }

    private:
        sem_t _sem;
    };

}

4. 模型demon代码

ring_queue.hpp

复制代码
#pragma once

#include "sem.hpp"
#include "mutex.hpp"
#include <iostream>
#include <vector>

using namespace sem_module;
using namespace mutex_module;

static const int default_cap = 5; // for test

template <typename T>
class ring_queue
{
public:
    ring_queue(int cap = default_cap)
        : _rq(cap),
          _cap(cap),
          _data_sem(0),
          _blank_sem(cap),
          _p_step(0),
          _c_step(0)
    {
    }

    // 生产者
    void product(const T &in)
    {
        // 申请位置信号量
        _blank_sem.P();
        // 先申请信号量,再加锁
        // 信号量的本质是对资源的预定机制,让每个消费者线程先预定自己的资源再来竞争锁拿到自己的资源
        {
            lock_guard lg(&_pmutex);

            // 生产
            _rq[_p_step] = in;
            // 更新下标
            _p_step++;
            _p_step %= _cap;
        }
        // 更新数据信号量
        _data_sem.v();
    }

    // 消费者
    void consum(T *out)
    {
        // 申请数据信号量
        _data_sem.P();
        {
            lock_guard lg(&mutex);
            // 消费
            *out = _rq[_c_step];
            // 更新下标
            _c_step++;
            _c_step %= _cap;
        }
        // 更新位置信号量
        _blank_sem.v();
    }
    ~ring_queue() {}

private:
    std::vector<T> _rq;
    int _cap;       // 循环队列容量
    sem _data_sem;  // 数据信号量
    sem _blank_sem; // 位置信号量
    int _p_step;    // 生产者下标
    int _c_step;    // 消费者下标

    // 维护多线程之间的互斥关系
    mutex _pmutex;
    mutex _cmutex;
};

main.cc

复制代码
​
#include "ring_queue.hpp"
#include <unistd.h>

// 生产者
void *product(void *args)
{
    int cnt = 1;
    ring_queue<int> *rq = static_cast<ring_queue<int> *>(args);
    while (true)
    {
        // sleep(1);
        rq->product(cnt);
        std::cout << "生产了:" << cnt << std::endl;
        cnt++;
    }
    return nullptr;
}

// 消费者
void *consum(void *args)
{
    int t;
    ring_queue<int> *rq = static_cast<ring_queue<int> *>(args);
    while (true)
    {
        sleep(1);
        rq->consum(&t);
        std::cout << "消费了:" << t << std::endl;
    }
    return nullptr;
}

int main()
{
    // 申请阻塞队列
    ring_queue<int> *rq = new ring_queue<int>();

    // 构建生产者和消费者
    pthread_t p, c;
    pthread_create(&p, nullptr, product, rq);
    pthread_create(&p, nullptr, consum, rq);

    pthread_join(p, nullptr);
    pthread_join(c, nullptr);

    return 0;
}

​