【Linux】多线程(下)

目录

一、生产者消费者模型

1.1 概念

1.2 基于阻塞队列

1.3 POSIX信号量

初始化信号量

销毁信号量

等待信号量

发布信号量

1.4 基于环形队列和POSIX信号量

二、线程池

2.1 概念

2.2 代码

三、封装Linux线程库

四、单例模式

4.1 概念

4.2 单例模式的实现方式

4.2.1 饿汉模式

4.2.2 懒汉模式

(1)线程不安全的懒汉模式

(2)线程安全的懒汉模式

①使用静态局部变量

②使用互斥锁

五、其他常见的锁

5.1 定义

5.2 C++11Atomic和CAS操作

六、读者写者问题

6.1 概念

6.2 读写策略

(1)读者优先

(2)写者优先

(3)读写公平

阅读本篇文章前推荐优先阅读:

【Linux】多线程(中)-优快云博客icon-default.png?t=O83Ahttps://blog.youkuaiyun.com/Eristic0618/article/details/143433347?spm=1001.2014.3001.5501


一、生产者消费者模型

1.1 概念

生产者消费者模型的产生是为了解决数据的生产者和消费者的强耦合问题,方案是提供一个额外的容器让生产者和消费者之间进行通讯。

其思路在于,生产者产出数据后不直接递交给消费者进行处理,而是托管到容器中;消费者也不会直接向生产者请求数据,而是从容器中获取。

在生活中,一个经典的生产者消费者模型就是超市了,客户们就是消费者,供货商们就是生产者,而超市就是容器。如果没有超市,客户只能和供货商直接对接,但单个客户的需求量对于供货商的生产计划而言实在是太少了,生产和消费的效率不平衡。超市就相当于一个大号的缓存,既能够承担供货商高效的生产效率,又能够接受客户零碎的消费需求,并且生产和消费不必同时进行,做到了将生产和消费动作很好的解耦,支持生产和消费的忙闲不均

在程序中,生产者和消费者就是一个个线程,数据存放到特定结构的内存空间中。于是这个内存空间一定会被多线程并发访问,是共享资源,所以我们的模型中一定要引入互斥锁保证其线程安全

生产者消费者模型中的“321原则”,分为三种关系、两个角色和一个交易场所,必须遵守

其中三种关系分为:

  • 生产者与生产者:互斥
  • 消费者与消费者:互斥
  • 生产者与消费者:互斥和同步

两个角色:生产者和消费者

一个交易场所:特定结构的内存空间

1.2 基于阻塞队列

阻塞队列(Blocking Queue)是一种常用于实现生产者消费者模型的数据结构,其特点在于当队列为空时,消费者从队列获取数据的动作将被阻塞,直到队列中有数据被放入;当队列已满时生产者向队列中存放数据的动作将被阻塞,直到队列中有空间

我们可以使用互斥锁和条件变量来实现基于阻塞队列的生产者消费者模型

//BlockQueue.hpp
#pragma once

#include <iostream>
#include <queue>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>

template <class T>
class BlockQueue
{
    static const int NUM = 5;
public:
    BlockQueue(int cap = NUM)
        :capacity_(cap)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&c_, nullptr);
        pthread_cond_init(&p_, nullptr);
    }

    const T& pop()
    {
        pthread_mutex_lock(&mutex_); //加锁
        while(q_.size() == 0) //阻塞队列为空(为什么要用while而不是if?)
        {
            pthread_cond_wait(&c_, &mutex_); //消费者等待
        }
        T out = q_.front(); //消费
        q_.pop();
        pthread_cond_signal(&p_); //唤醒生产者
        pthread_mutex_unlock(&mutex_); //解锁
        return out;
    }

    void Push(const T& in)
    {
        pthread_mutex_lock(&mutex_); //加锁
        while(q_.size() == capacity_) //阻塞队列已满
        {
            pthread_cond_wait(&p_, &mutex_); //生产者等待
        }
        q_.push(in); //生产
        pthread_cond_signal(&c_); //唤醒消费者
        pthread_mutex_unlock(&mutex_); //解锁
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&c_);
        pthread_cond_destroy(&p_);
    }

private:
    std::queue<T> q_;
    int capacity_; //阻塞队列容量
    pthread_mutex_t mutex_; //互斥锁
    pthread_cond_t c_;
    pthread_cond_t p_;
};

可以看到在Pop和Push函数中,我们判断队列为空或已满时使用的是while循环而不是if,为什么?

答案:防止线程被伪唤醒的情况

假设队列容量已满,消费者在消费完毕后可能在唤醒的时候一次性唤醒了多个生产者,被唤醒的这几个生产者先对锁竞争,第一个竞争到锁的线程开始生产,生产完毕后释放锁。接着消费者和剩余被唤醒的生产者共同竞争这把锁,如果下一个竞争到锁的还是生产者,则可能导致生产了超出队列容量的数据,导致错误。

使用while循环,就可以做到在线程被伪唤醒的情况下重新将其加入条件变量中

在main.cc中调用BlockQueue.hpp,实现一个多生产多消费的情景

#include "BlockQueue.hpp"

pthread_mutex_t mutex; //互斥锁

void* Consumer(void* args) //消费者例程
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    while(true)
    {
        sleep(1); //可以让消费的速度慢一些
        int data = bq->pop(); //消费数据
        std::cout << "consuming a data: " << data << std::endl;
    }
}

void* Productor(void* args) //生产者例程
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    static int data = 0; //模拟数据
    while (true)
    {
        pthread_mutex_lock(&mutex);
        data++; //临界资源
        bq->Push(data); //生产数据
        std::cout << "produce a data: " << data << std::endl;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    BlockQueue<int> *bq = new BlockQueue<int>();
    pthread_t c[3], p[5]; 
    for (int i = 0; i < 3; i++) //创建多个消费者线程
    {
        pthread_create(c + i, nullptr, Consumer, bq);
    }
    for (int i = 0; i < 5; i++) //创建多个生产者线程
    {
        pthread_create(p + i, nullptr, Productor, bq);
    }
    //线程等待
    for (int i = 0;i < 3; i++)
    {
        pthread_join(c[i], nullptr);
    }
    for (int i = 0; i < 5; i++)
    {
        pthread_join(p[i], nullptr);
    }
    delete bq;
    return 0;
}

程序运行结果如下:

因为我们的代码中选择让消费者的速度慢一些,对生产者的速度不作限制,因此可以看到一旦消费者消费一个数据,生产者就立刻生产数据补上

在实际情况中,不一定只有队列为空才让消费者等待,队列为满让生产者等待,而是可以添加一个水位线,限定数据存量的上限和下限

为何生产者消费者模型是高效的?在业务中,生产者生产的数据往往需要花费时间获取,消费者在获取数据后也可能要对数据进行加工处理,也需要时间。生产者在获取数据时消费者可能在消费数据,生产者在生产数据时消费者可能正在加工处理数据,此时一个访问临界区,一个访问非临界区,二者互不干扰。

虽然生产和消费时线程之间是互斥的,但多生产多消费的意义在于让线程能够并发的完成数据的获取和后续数据的加工处理

1.3 POSIX信号量

在前面我们已经学习过了System V信号量

【Linux】进程间通信——System V消息队列和信号量_vos消息队列-优快云博客icon-default.png?t=O83Ahttps://blog.youkuaiyun.com/Eristic0618/article/details/142635584?spm=1001.2014.3001.5501POSIX信号量和System V信号量作用相同,但POSIX信号量可以用于线程间同步。

之前提到过,信号量的本质就是一把计数器,我们只要让信号量的值与资源的数量保持一致,让线程竞争信号量,那么每一个竞争到信号量的线程就一定会分配到资源。也就是说,在申请信号量和释放信号量之间,无需再对资源是否就绪做判断了,因为持有信号量就意味着一定有一份资源属于该线程

这里不再赘述信号量相关的概念,只需要了解其API如何使用即可

初始化信号量

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

其中sem是待初始化的POSIX信号量;pshared为0表示线程间共享,非0表示进程间共享;value为信号量初始值

销毁信号量

#include <semaphore.h>

int sem_destroy(sem_t *sem);

等待信号量

#include <semaphore.h>

int sem_wait(sem_t *sem);

线程调用sem_wait函数后会将信号量sem的值减1

发布信号量

#include <semaphore.h>

int sem_post(sem_t *sem);

资源使用完毕后,线程可

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值