Bootstrap

Linux线程(三)—— 多线程(生产者消费者模型、信号量、线程池)

生产者消费者模型

生产者消费者模型中(321原则)

3种关系

生产者和生产者 (互斥关系) 消费者和消费者 (互斥关系) 生产者和消费者 (同步关系、互斥关系)

2种角色:生产者和消费者

1个交易场所:通常是缓冲区

为何要使用生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

生产者消费者模型优点

  • 解耦
  • 支持并发
  • 支持忙闲不均

为什么说生产者消费者模型高效呢?

基于BlockingQueue的生产者消费者模型

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。

阻塞队列C++代码

单生产单消费(只要维护生产者和消费者的关系)

//cp.cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include "BlockQueue.hpp"
using namespace std;

void* consumer(void* args)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int> *>(args);
    while(true)
    {
        sleep(1);
        //1. 将数据从BlockQueue中获取 -- 获取到了数据
        int data;
        bq->pop(&data);
        //2. 结合某种业务逻辑,处理数据
        cout << "消费者获得了: " << data << endl;
    }

}

void* producer(void* args)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int> *>(args);
    while(true)
    {
        //1. 先通过某种渠道获取数据
        int data = rand() % 10 + 1;
        //2. 将数据推送到BlockQueue -- 完成生产过程
        bq->push(data);

        cout << "生产者生产了: " << data << endl;
    }

}

int main()
{
    srand((uint64_t)time(nullptr));
    //单生产单消费  ————  只要维护生产者和消费者的关系
    pthread_t c,p;
    BlockQueue<int>* bq = new BlockQueue<int>();
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, producer, bq);

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

    delete bq;
    return 0;
}
//BlockQueue.hpp
#pragma once

#include <iostream>
#include <queue>
#include <pthread.h>
using namespace std;

const int g_capacity = 5;

template<class T>
class BlockQueue
{
public:
    BlockQueue(size_t capacity = g_capacity)
        :_capacity(capacity)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_consumerCond, nullptr);
        pthread_cond_init(&_producerCond, nullptr);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumerCond);
        pthread_cond_destroy(&_producerCond);
    }

    void push(const T& data)
    {
        pthread_mutex_lock(&_mutex);
        //一定要用while循环判断,因为要保证在任何时候,满足条件了才能去生产。
        while(IsFull())    //1. 只能在临界区内部判断临界资源是否就绪!注定了在当前一定是持有锁的
        {
            //2. 要让线程进行休眠等待,不能持有锁等待!
            //3. 注定了, pthread_cond_wait要有锁的释放能力!
            pthread_cond_wait(&_producerCond, &_mutex);     
            //当前线程休眠(切换)了, 被唤醒后,在哪里继续向后执行呢?
            //4. 当线程醒来时, 继续从临界区内部继续运行! 因为当前线程是在临界区内被切换走的.
            //5. 当线程被唤醒时,继续在pthread_cond_wait函数向后运行,又要重新申请锁, 申请成功才会彻底返回!
        }
        _q.push(data);
        pthread_cond_signal(&_consumerCond);    //通知消费者, 队列中有数据了

        pthread_mutex_unlock(&_mutex);
    }
    void pop(T* data)
    {
        pthread_mutex_lock(&_mutex);
        while(IsEmpty())   //如果队列为空, 则让消费者等待
        {
            pthread_cond_wait(&_consumerCond, &_mutex);
        }
        *data = _q.front();
        _q.pop();
        pthread_cond_signal(&_producerCond);    //通知生产者, 队列中有可用空间了
        pthread_mutex_unlock(&_mutex);
    }

    bool IsFull()
    {
        return _q.size() == _capacity;
    }
    bool IsEmpty()
    {
        return _q.empty();
    }
private:
    queue<T> _q;
    size_t _capacity;
    pthread_mutex_t _mutex;
    pthread_cond_t _consumerCond;   //消费者对应的条件变量,队列为空时,在此wait
    pthread_cond_t _producerCond;   //生产者对应的条件变量,队列为满时,在此wait
};

引入任务处理,生产者生产任务,消费者处理任务

//Task.hpp
#pragma once
#include <iostream>
#include <string>

class Task
{
public:
    Task()
    {

    }
    Task(int x, int y, int op)
        :_x(x)
        ,_y(y)
        ,_op(op)
        ,_result(0)
        ,_exitCode(0)
    {}

    void operator()()
    {
        switch(_op)
        {
            case '+':
                _result = _x + _y;
                break;
            
            case '-':
                _result = _x - _y;
                break;
            
            case '*':
                _result = _x * _y;
                break;

            case '/':
                if(_y == 0)
                    _exitCode = -1;
                else
                    _result = _x / _y;
                break;
            
            case '%':
                if(_y == 0)
                    _exitCode = -2;
                else
                    _result = _x % _y;
                break;
        }
    }

    std::string PrintArg()
    {
        return std::to_string(_x) + _op + std::to_string(_y) + "=";
    }

    std::string PrintRes()
    {
        return std::to_string(_result) + "(" + std::to_string(_exitCode) + ")";
    }

private:
    int _x;
    int _y;
    char _op;

    int _result;
    int _exitCode;
};
//BlockQueen.hpp
#pragma once

#include <iostream>
#include <queue>
#include <pthread.h>
using namespace std;

const int g_capacity = 5;

template<class T>
class BlockQueue
{
public:
    BlockQueue(size_t capacity = g_capacity)
        :_capacity(capacity)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_consumerCond, nullptr);
        pthread_cond_init(&_producerCond, nullptr);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumerCond);
        pthread_cond_destroy(&_producerCond);
    }

    void push(const T& data)
    {
        pthread_mutex_lock(&_mutex);
        //一定要用while循环判断,因为要保证在任何时候,满足条件了才能去生产。
        while(IsFull())    //1. 只能在临界区内部判断临界资源是否就绪!注定了在当前一定是持有锁的
        {
            //2. 要让线程进行休眠等待,不能持有锁等待!
            //3. 注定了, pthread_cond_wait要有锁的释放能力!
            pthread_cond_wait(&_producerCond, &_mutex);     
            //当前线程休眠(切换)了, 被唤醒后,在哪里继续向后执行呢?
            //4. 当线程醒来时, 继续从临界区内部继续运行! 因为当前线程是在临界区内被切换走的.
            //5. 当线程被唤醒时,继续在pthread_cond_wait函数向后运行,又要重新申请锁, 申请成功才会彻底返回!
        }
        _q.push(data);
        pthread_cond_signal(&_consumerCond);    //通知消费者, 队列中有数据了

        pthread_mutex_unlock(&_mutex);
    }
    
    void pop(T* data)
    {
        pthread_mutex_lock(&_mutex);
        while(IsEmpty())   //如果队列为空, 则让消费者等待
        {
            pthread_cond_wait(&_consumerCond, &_mutex);
        }
        *data = _q.front();
        _q.pop();
        pthread_cond_signal(&_producerCond);    //通知生产者, 队列中有可用空间了
        pthread_mutex_unlock(&_mutex);
    }

    bool IsFull()
    {
        return _q.size() == _capacity;
    }
    bool IsEmpty()
    {
        return _q.empty();
    }
private:
    queue<T> _q;
    size_t _capacity;
    pthread_mutex_t _mutex;
    pthread_cond_t _consumerCond;   //消费者对应的条件变量,队列为空时,在此wait
    pthread_cond_t _producerCond;   //生产者对应的条件变量,队列为满时,在此wait
};
//cp.cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include "BlockQueue.hpp"
#include "Task.hpp"
using namespace std;

void* consumer(void* args)
{
    BlockQueue<Task>* bq = static_cast<BlockQueue<Task> *>(args);
    while(true)
    {
        sleep(1);
        //1. 将数据从BlockQueue中获取 -- 获取到了数据
        Task t;
        bq->pop(&t);
        //2. 结合某种业务逻辑,处理数据
        t();
        cout << "消费者获得了: " << t.PrintArg() << t.PrintRes() << endl;
    }

}

void* producer(void* args)
{
    BlockQueue<Task>* bq = static_cast<BlockQueue<Task> *>(args);
    string operations = "+-*/%";
    while(true)
    {
        //1. 先通过某种渠道获取数据
        int x = rand() % 20 + 1;
        int y = rand() % 10 + 1;
        char op = operations[rand() % operations.size()];
        Task t(x, y, op);
        //2. 将数据推送到BlockQueue -- 完成生产过程
        bq->push(t);

        cout << "生产者生产了: " << t.PrintArg() << "?" << endl;
    }

}

int main()
{
    srand((uint64_t)time(nullptr));
    //单生产单消费  ————  只要维护生产者和消费者的关系
    pthread_t c,p;
    BlockQueue<Task>* bq = new BlockQueue<Task>();
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, producer, bq);

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

    delete bq;
    return 0;
}

多生产者多消费者

//只需增加线程
//cp.cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include "BlockQueue.hpp"
#include "Task.hpp"
using namespace std;

void* consumer(void* args)
{
    BlockQueue<Task>* bq = static_cast<BlockQueue<Task> *>(args);
    while(true)
    {
        sleep(1);
        //1. 将数据从BlockQueue中获取 -- 获取到了数据
        Task t;
        bq->pop(&t);
        //2. 结合某种业务逻辑,处理数据
        t();
        cout << pthread_self() << " 消费者获得了: " << t.PrintArg() << t.PrintRes() << endl;

    }

}

void* producer(void* args)
{
    BlockQueue<Task>* bq = static_cast<BlockQueue<Task> *>(args);
    string operations = "+-*/%";
    while(true)
    {
        //1. 先通过某种渠道获取数据
        int x = rand() % 20 + 1;
        int y = rand() % 10 + 1;
        char op = operations[rand() % operations.size()];
        Task t(x, y, op);
        //2. 将数据推送到BlockQueue -- 完成生产过程
        bq->push(t);

        cout << pthread_self() << " 生产者生产了: " << t.PrintArg() << "?" << endl;
    }
}

int main()
{
    srand((uint64_t)time(nullptr));
    //单生产单消费  ————  只要维护生产者和消费者的关系
    pthread_t c[2],p[2];
    BlockQueue<Task>* bq = new BlockQueue<Task>();
    pthread_create(&c[0], nullptr, consumer, bq);
    pthread_create(&c[1], nullptr, consumer, bq);
    pthread_create(&p[0], nullptr, producer, bq);
    pthread_create(&p[1], nullptr, producer, bq);

    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);

    delete bq;
    return 0;
}

POSIX信号量

信号量:本质就是一个计数器

信号量需要进行PV操作,P:— V:++

一定要是原子的!

信号量用于描述临界资源中的资源数目的!

每一个线程,在访问对应的资源的时候,先申请信号量,申请成功,表示改线程允许使用该资源,申请不成功,目前无法使用该资源!

信号量的工作机制:类似于看电影买票,是一种资源的预定机制!

而信号量已经是资源的计数器了,申请信号量成功,就表明资源可用!申请信号量失败就表明资源不可用——本质就是把判断转换为信号量的申请行为!

信号量操作

初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:

pshared: 0表示线程间共享,非零表示进程间共享

value:信号量初始值

销毁信号量

int sem_destroy(sem_t *sem);

等待信号量

功能:等待信号量,会将信号量的值减1

int sem_wait(sem_t *sem); //P()

发布信号量

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。

int sem_post(sem_t *sem);//V()

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

构建CP问题

  1. 生产者和消费者关系的”资源”,是一样的吗?

    不一样,生产者关心空间,消费者关心数据

  2. 只要信号量不为0,表示资源可用,表示线程可访问

  3. 环形队列只要访问不同的区域,生产和消费行为可以同时进行吗?

    可以

  4. 什么时候会访问同一个区域?

    刚开始,队列为空时 指向同一个位置,存在竞争关系,让生产者先运行。

    队列为满时 也存在竞争关系,让消费者先运行。

    代码

    //RingQueue.hpp
    #pragma once
    #include <iostream>
    #include <vector>
    #include <pthread.h>
    #include <semaphore.h>
    using namespace std;
    
    const int N = 5;
    
    template<class T>
    class RingQueue
    {
    private:
        void Lock(pthread_mutex_t& mtx)
        {
            pthread_mutex_lock(&mtx);
        }
        void Unlock(pthread_mutex_t& mtx)
        {
            pthread_mutex_unlock(&mtx);
        }
        void P(sem_t& sem)
        {
            sem_wait(&sem);
        }
        void V(sem_t& sem)
        {
            sem_post(&sem);
        }
    public:
        RingQueue(const int& num = N)
            :_num(num)
            ,arr(num)
        {
            pthread_mutex_init(&_cmtx, nullptr);
            pthread_mutex_init(&_pmtx, nullptr);
    
            sem_init(&_space_sem, 0, num);
            sem_init(&_data_sem, 0, 0);
            
            _cstep = _pstep = 0;
        }
    
        ~RingQueue()
        {
            pthread_mutex_destroy(&_cmtx);
            pthread_mutex_destroy(&_pmtx);
    
            sem_destroy(&_space_sem);
            sem_destroy(&_data_sem);
        }
    	
        void push(const T& in)
        {
    				// 1. 可以不用在临界区内部做判断,就可以知道临界资源的使用情况
            // 2. 什么时候用锁,什么时候用sem?你对应的临界资源,是否被整体使用
            P(_space_sem);
            Lock(_pmtx);
            
            arr[_pstep++] = in;
            _pstep %= _num; 
    
            Unlock(_pmtx);
            V(_data_sem);
        }
        
        void pop(T& out)
        {
            P(_data_sem);
            Lock(_cmtx);
    
            out = arr[_cstep++];
            _cstep %= _num;
    
            Unlock(_cmtx);
            V(_space_sem);
        }
    
    private:
        vector<T> arr;  //临界资源
        int _num;       //队列大小
    
        int _cstep;     //消费位置
        int _pstep;     //生产位置
    
        sem_t _space_sem;       //空间信号量    生产者关心
        sem_t _data_sem;        //数据信号量    消费者关心
    
        pthread_mutex_t _cmtx;      //消费者锁
        pthread_mutex_t _pmtx;      //生产者锁
    
    };
    
    //cp.cpp
    #include "RingQueue.hpp"
    #include "Task.hpp"
    #include <string>
    #include <unistd.h>
    
    void* consumer(void* args)
    {
        RingQueue<Task>* rq = static_cast<RingQueue<Task>*> (args);
        Task t;
        while(true)
        {
            rq->pop(t);
            t();
            cout << "消费者" << pthread_self() << "消费了数据:" << t.PrintRes() << endl;
        }
    }
    
    void* producer(void* args)
    {
        RingQueue<Task>* rq = static_cast<RingQueue<Task>*> (args);
        string operators = "+-*/%";
        while(true)
        {
            int x = rand() % 100 + 1;
            int y = rand() % 100 + 1;
            char op = operators[rand() % operators.size()];
            Task t(x, y, op);
            rq->push(t);
            cout << "生产者" << pthread_self() << "生产了数据:" << t.PrintArg() << endl;
            //sleep(1);
        }
    
    }
    
    int main()
    {
        srand(time(nullptr) ^ pthread_self());
        RingQueue<Task>* rq = new RingQueue<Task>();
    
        //单生产单消费
        // pthread_t c,p;
        // pthread_create(&c, nullptr, consumer, rq);
        // pthread_create(&p, nullptr, producer, rq);
    
        // pthread_join(c, nullptr);
        // pthread_join(p, nullptr);
    
        //多生产多消费
        pthread_t c[3], p[2];
        for(int i = 0; i < 3; i++)
            pthread_create(c + i, nullptr, consumer, rq);
        for(int i = 0; i < 2; i++)
            pthread_create(p + i, nullptr, producer, rq);
    
        for(int i = 0; i < 3; i++)
            pthread_join(c[i], nullptr);
        for(int i = 0; i < 2; i++)
            pthread_join(p[i], nullptr);
    
        
        return 0;
    }
    
    //Task.hpp
    #pragma once
    #include <iostream>
    #include <string>
    #include <unistd.h>
    
    class Task
    {
    public:
        Task()
        {
    
        }
        Task(int x, int y, int op)
            :_x(x)
            ,_y(y)
            ,_op(op)
            ,_result(0)
            ,_exitCode(0)
        {}
    
        void operator()()
        {
            switch(_op)
            {
                case '+':
                    _result = _x + _y;
                    break;
                
                case '-':
                    _result = _x - _y;
                    break;
                
                case '*':
                    _result = _x * _y;
                    break;
    
                case '/':
                    if(_y == 0)
                        _exitCode = -1;
                    else
                        _result = _x / _y;
                    break;
                
                case '%':
                    if(_y == 0)
                        _exitCode = -2;
                    else
                        _result = _x % _y;
                    break;
            }
            usleep(10000);
        }
    
        std::string PrintArg()
        {
            return std::to_string(_x) + _op + std::to_string(_y) + "= ?";
        }
    
        std::string PrintRes()
        {
            return std::to_string(_result) + "(" + std::to_string(_exitCode) + ")";
        }
    
    private:
        int _x;
        int _y;
        char _op;
    
        int _result;
        int _exitCode;
    };
    

线程池

一种线程使用模式。本质是一个生产者消费者模型。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。

线程池的应用场景:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

STL、智能指针、线程安全

STL中的容器是线程安全的吗?

不是

智能指针是不是线程安全的?

unique_ptr, 不涉及线程安全问题

shared_ptr 不是线程安全的

;