【线程封装及各种测试代码--阻塞队列、环形队列、线程池】

前言

打怪升级:第80天

本文为之前讲解内容的测试实战,下方对系统调用中的线程、互斥量、条件变量以及信号量进行了适当的封装,
并且采用生产者消费者模型来进行测试,感兴趣的朋友大可一看,如有疑问,我们评论区见~。


线程 – Thread.hpp

#pragma once
#include<iostream>
#include<pthread.h>
//#include<functional>
#include<string>
using namespace std;

//typedef function<void*(void*)> func_t;
typedef void* (*func_t)(void*);

class Thread
{
public:
    Thread(int num, func_t f, void *arg):_routine(f), _arg(arg)
    {
        _tname = "thread_" + to_string(num);
    }
  
    //  2. 为什么传this指针? -- 由于running为静态成员,没有this指针,因此为了在running函数中回调routine函数,需要传入this
    void run(){  pthread_create(&_tid, nullptr, routine, this); }// ?

    void join() {  pthread_join(_tid, nullptr); }

    string &get_tname() { return _tname; }

    pthread_t &get_tid() { return _tid; }

    ~Thread() {}

private:
    func_t get_routine() { return _routine; }

    void *get_arg() { return _arg; }

    // 1.为什么使用static? -- running为成员函数,加static是为了消去this指针
    static void *routine(void * arg) 
    {
        Thread *pt = static_cast<Thread *>(arg);
        std::cout << "create thread: " << pt->get_tname() << std::endl;
        pt->get_routine()(pt->get_arg());
    } 

private:
    func_t _routine;  // 线程入口函数
    void *_arg;      // 参数
    string _tname;   // 线程名
    pthread_t _tid;  // 线程id -- 地址
};

锁 – Mutex.hpp

#pragma once

#include<pthread.h>

class Mutex
{  
  public:
    Mutex():_pmtx(new pthread_mutex_t){}

    void init(){pthread_mutex_init(_pmtx, nullptr);}

    void destroy(){pthread_mutex_destroy(_pmtx);}

    void lock() {pthread_mutex_lock(_pmtx);}

    void unlock(){pthread_mutex_unlock(_pmtx);}

    pthread_mutex_t * get_ptr_mutex() {return _pmtx;}

    ~Mutex(){ delete _pmtx;}

  private:
    pthread_mutex_t *_pmtx; // 封装的锁的指针
};

条件变量 – Condition.hpp

#pragma once

#include<pthread.h>
#include"Mutex.hpp"

class Condition
{
public:
    Condition():_pcond(new pthread_cond_t){}

    void init(){pthread_cond_init(_pcond, nullptr);}

    void destroy(){pthread_cond_destroy(_pcond);}

    void wait(Mutex& mtx){pthread_cond_wait(_pcond, mtx.get_ptr_mutex());};

    void signal(){pthread_cond_signal(_pcond);}

    void allsignal(){pthread_cond_broadcast(_pcond);}

    ~Condition(){ delete _pcond;}
private:
    pthread_cond_t *_pcond;  // 封装条件变量指针
};

实例1:阻塞队列 – BlockQueue.hpp

#include<queue>
#include"Mutex.hpp"
#include"Condition.hpp"

const int N = 5; // 默认队列大小
template<class T>
class blockqueue
{
public:
blockqueue(int cnt = N):_cap(cnt)
    {
        _mtx.init();
        _consumer_cond.init();
        _producer_cond.init();
    }

void push(const T &val)
{
    _mtx.lock();

    //if(_q.size() == _cap)  // ??
    // 多生产多消费情况 -- 多个生产者都在条件变量上阻塞,如果使用广播唤醒,且此时只有一个空位
    // 则多个生产者会同时去竞争锁,之后醒来,如果多个生产者轮次获得了锁,此时因为已经走过了判断容量的过程,因此之后不会再次判断,
    // 直接往后进 --- 开始放入产品,就会超出阻塞队列容量,因此唤醒之后需要再次判断是否有空位。

    while(_q.size() == _cap) 
    {
        _producer_cond.wait(_mtx);
    }

    _q.push(val);
    // 互相唤醒  -- 放入一个,我能保证现在至少有一个产品,叫醒消费者来消费
    // 可以加条件判断 -- 生产产品数大于容量的一半了再唤醒消费者
    //if(_q.size() > _cap / 2) 
       _consumer_cond.signal();

    _mtx.unlock();
}

void pop(T &val)
{
    _mtx.lock();

    while(_q.empty()) 
    {
        _consumer_cond.wait(_mtx);
    }

    val = _q.front();
    _q.pop();

    //if(_q.size() < _cap / 2)
        _producer_cond.signal();

    _mtx.unlock();
}

    ~blockqueue()
    {
        _mtx.destroy();
        _consumer_cond.destroy();
        _producer_cond.destroy();
    }

private:
queue<T> _q;               // 模拟阻塞队列
int _cap;                  // 限制队列容量
Mutex _mtx;                // 对阻塞队列的互斥访问
Condition _consumer_cond;  // 消费者取出条件
Condition _producer_cond;  // 生产者放入条件
};
运行测试

多生产多消费模型,限制阻塞队列容量
两种场景:

    1. 生产者速度快
    1. 消费者速度快
#include<iostream>
#include<unistd.h>
#include"Thread.hpp"
#include"Blockqueue.hpp"
#include<string>

using namespace std;

// 封装一个线程库,封装传参、执行、等待等库接口
// 模拟实现 -- 数据队列 -- cp模型

void *consumer_routine(void *arg)
{
    blockqueue<int> *bq = static_cast<blockqueue<int> *>(arg);
    while(true)
    {
        sleep(1);     // 消费减速
        int val;
        bq->pop(val);
        cout << "consumer pop: " << val << endl;
    }   
}

void *producer_routine(void *arg)
{
    blockqueue<int> *bq = static_cast<blockqueue<int> *>(arg);
    while(true)
    {
//        sleep(1);     // 生产减速
        int val = rand() % 100;
        bq->push(val);
        cout << "producer push: " << val << endl;
    }
}

int main()
{
    srand(time(0));
    blockqueue<int> bq;
    pthread_t cons[2], prod[3];   // 多生产多消费
    for(int i=0; i<2; ++i)
        pthread_create(cons + i, nullptr, consumer_routine, (void *)&bq);
    for(int i=0; i<3; ++i)
        pthread_create(prod + i, nullptr, producer_routine, (void *)&bq);

   for(int i=0; i<2; ++i)
        pthread_join(cons[i], nullptr);
   for(int i=0; i<3; ++i) 
        pthread_join(prod[i], nullptr);

    cout << "all thread exit" << endl;

    return 0;
}

1.生产者速度快
在这里插入图片描述
现象:瞬间放满阻塞队列,之后消费一个生产一个。

2.消费者速度快在这里插入图片描述
实验现象:生产一个消费一个。


信号量 – Semaphore.hpp

#include<semaphore.h>

class Semaphore
{
public:
    Semaphore(int pORt, int num):_psem(new sem_t) { sem_init(_psem, pORt, num); }

    void P() { sem_wait(_psem); }

    void V() { sem_post(_psem); }

    ~Semaphore() { sem_destroy(_psem); }

private:  
sem_t *_psem;
};

任务 – Task.hpp

#include<string>   //   to_string

class Task  // 五则运算
{
  public:

    Task() {}

    Task(int a, int b, char op):_a(a), _b(b), _op(op) {}

    void execute()
    {
        switch(_op)
        {
            case '+': _result = _a + _b; break;
            case '-': _result = _a - _b; break;
            case '*': _result = _a * _b; break;
            case '/':
            {
                if(_b == 0) throw("is division zero");  // 抛异常
                else _result = _a / _b;
                break;  
            } 
            case '%': 
            {
                if(_b == 0) throw("is mod zero");
                else _result = _a % _b;
                break;  
            } 
        }
    }

    string get_exp() // 公式
    {
        return std::to_string(_a) + _op +  std::to_string(_b) + " = ";
    }

    int get_res()  //   结果 
    {
        return _result;
    }

    ~Task(){}

  private:  
  int _a;
  int _b;
  char _op;
  int _result;
};

实例2:环形队列 – RingQueue.hpp

#pragma once

#include<vector>
#include"Mutex.hpp"
#include"Semaphore.hpp"

const int N = 5;

template<class T>
class ringqueue
{
public:
    ringqueue(int cap = N):_v(cap), _cap(cap),_c_sem(0, 0), _p_sem(0, cap){}

    void push(const T& val)
    {
        // 下标重叠位置:
        // 1.最开始环形队列中资源为0个,一定是生产者先运行,消费者阻塞在条件变量上
        // 2.队列数据已满,一定是消费者先运行,生产者阻塞在条件变量上,因此无需手动控制
        _p_sem.P();
        _p_mtx.lock();
        _v[_p_sub++] = val;
        _p_sub %= _cap;
        _p_mtx.unlock();
        _c_sem.V();
    }

    void pop(T& val)
    {
        _c_sem.P();
        _c_mtx.lock();
        val = _v[_c_sub++];
        _c_sub %= _cap;
        _c_mtx.unlock();
        _p_sem.V();
    }

    ~ringqueue(){}
private:
vector<T> _v;
int _cap;
Mutex _c_mtx;         // 互斥访问队列 -- 多个消费者同时访问一块资源,需要互斥访问
Mutex _p_mtx;         // 互斥访问队列 -- 多个生产者同时访问一块资源,需要互斥访问
int _c_sub = 0;      // 准备读取的位置
int _p_sub = 0;      // 准备写入的位置
Semaphore _c_sem; // 消费信号量,记录可以消费的数据个数
Semaphore _p_sem; // 生产信号量,记录可以生产的数据个数
};

环形队列与阻塞队列的区别: 作为整体的目标不同;
阻塞队列把整个队列作为一个整体: 生产者消费者共用一把锁, 同一时间只允许进行一种操作,不伦是生产还是消费.
环形队列将数组中的每一个元素作为一个整体: 生产者和消费者各有各的锁, 除非是访问同一块区域(此时会通过信号量来阻塞一种操作), 其他情况下, 因为访问的是不同的整体, 生产和消费可以同时进行.

运行测试

单生产单消费模型,限制阻塞队列容量
两种场景:

    1. 生产者速度快
    1. 消费者速度快
      (也满足多生产多消费)
#include<iostream>
#include<unistd.h>
#include<cstring>
#include"Thread.hpp"
#include"Ringqueue.hpp"
#include"Task.hpp"

using namespace std;

// 模拟实现 -- 环形队列 -- 信号量 -- cp模型

void *consumer_routine(void *arg)
{
    ringqueue<Task> *rq = static_cast<ringqueue<Task> *>(arg);
    while(true)
    {
        sleep(1);
        Task t;
        rq->pop(t);
        try
        {
            t.runtask();
            cout << "consumer pop: " << t.get_exp() << t.get_res() << endl;
        }
        catch(const char *s)
        {
            cerr <<"exception: " << s << endl;
        }
    }   
}

void *producer_routine(void *arg)
{
    ringqueue<Task> *rq = static_cast<ringqueue<Task> *>(arg);
    while(true)
    {
        char opArr[] = "+-*/%";
        int a = rand() % 5;
        int b = rand() % 5;
        int opi = rand() % strlen(opArr);
        Task t(a, b, opArr[opi]);
        rq->push(t);
        cout << "producer push: " << t.get_exp() + '?' << endl;
    }
}

int main()
{
    srand(time(0));
    ringqueue<Task> rq;
    thread t1(consumer_routine, (void *)&rq, "consumer thread");
    thread t2(producer_routine, (void *)&rq, "producer thread");

    t1.run();
    t2.run();

    t1.join();
    t2.join();

    cout << "all thread exit" << endl;

    return 0;
}

1.生产者速度快
在这里插入图片描述
现象:瞬间放满阻塞队列,之后消费一个生产一个。

2.消费者速度快
在这里插入图片描述
实验现象:生产一个消费一个。


实例3:线程池 – ThreadPool.hpp

#include<vector>
#include<queue>
#include<iostream>
#include"Thread.hpp"

const static int N = 5; 

template<class T>
class ThreadPool
{
private:      //  简单封装
    void lock() { pthread_mutex_lock(&_mtx); }
    void unlock() { pthread_mutex_unlock(&_mtx); }
    void wait() { pthread_cond_wait(&_cond, &_mtx); }
    void signal() { pthread_cond_signal(&_cond); }
    bool isEmpty() { return _tasks.empty(); }
    
    static void * routine(void *arg)   // 3.  线程们会在这里等待任务的到来, 如果没有任务就会阻塞
    {
        ThreadPool<T> * tp = static_cast<ThreadPool<T> *>(arg);

        while(true)
        {
            tp->lock();
            while(tp->isEmpty()) tp->wait();
            T t;
            tp->taskPop(t);
            tp->unlock();
            try
            {
                t.execute();
            }
            catch(const char *s)
            {
                std::cerr << s << endl;
            }
            
            cout << t.get_exp() << t.get_res() << endl;
        }
    }

public:
    ThreadPool(int num = N) :_num(num)
    {
        pthread_mutex_init(&_mtx, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    void init()           //  1. 线程们初始化, 使用需要的数据初始化Thread对象
    {
        for(int i=0; i<_num; ++i)
        {
            _threads.push_back(Thread(i, routine, this));
        }
        cout << "all thread init" << endl;
    }

    void start()        //  2.  启动线程, 线程们会进入routine函数
    {
        for(int i=0; i<_num; ++i)
        {
            _threads[i].run();
        }
        cout << "all thread start" << endl;
    }

    void taskPush(const T t)
    {
        lock();
        _tasks.push(t);
        unlock();
        signal();        // 发排任务,唤醒执行
    }

    void taskPop(T &t)
    {
        // lock();   **加锁的情况下才能够进来取数据,因此此时不需要加锁,否则死锁**
        t = _tasks.front();
        _tasks.pop();
        // unlock();
    }

    ~ThreadPool() 
    {
        pthread_mutex_destroy(&_mtx);
        pthread_cond_destroy(&_cond);
    }

private:
    vector<Thread> _threads;         // 线程数组
    int _num; 						//  线程个数 -- 虽然vector本身就可以获取这个数据, 不过加上方便使用
    queue<T> _tasks;				// 任务队列
    pthread_mutex_t _mtx;           // 互斥访问任务队列
    pthread_cond_t _cond;           // 无任务,线程阻塞
};
运行测试

单生产多消费,
由于没有像上方一样现在任务队列容量,因此不方便观察“生产者速度快”的实验现象,
此处只演示消费者速度快。

#include<iostream>
#include<memory>         // unique_ptr
#include<cstring>        // strlen
#include<unistd.h>       // sleep
using namespace std;
#include"Task.hpp"
#include"ThreadPool_v2.hpp"

int main()
{
    unique_ptr<ThreadPool<Task> > tp(new ThreadPool<Task>());

    tp->init();
    tp->start();

    const char *ops = "+-*/%";
    while(true)                  // 生产者生产任务
    {
        int a = rand() % 100;
        int b = rand() % 100;
        int opi = rand() % strlen(ops);
        tp->taskPush(Task(a, b, ops[opi]));
        cout << "push task..." << endl;
        sleep(1);               // 生产者速度慢
    }

    return 0;
}

在这里插入图片描述> 实验现象:生产一个消费一个。

总结

使用条件变量若进行等待,会自动释放锁,且在被唤醒之后会再次申请锁.
因此pthread_cond_wait的第二个参数为 锁的指针,当该条件变量上的进程被唤醒时会自动去申请锁,只有当再次申请到锁时才会继续往后运行,否则会重新在锁上阻塞。

何时使用锁,何时使用信号量:
一份资源当做整体去访问时要加锁,上方的阻塞队列;
一份资源分多块访问时使用信号量,表示可访问资源,或资源分块数量,上方的环形队列。



  • 16
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
AQS(AbstractQueuedSynchronizer)是Java中实现同步器的框架,它提供了一种基于FIFO队列的阻塞和唤醒机制。AQS的阻塞队列原理是通过CLH(Craig, Landin, and Hagersten)队列来实现的。 CLH队列是一种虚拟的双向链表,它仅存在节点之间的关联关系,而不存在队列的实例。每个请求共享资源的线程都会被封装成一个CLH队列的节点(Node)。当线程请求共享资源时,它会被添加到CLH队列的尾部,并进入阻塞状态。 当共享资源被占用时,其他线程请求该资源的线程会被放入CLH队列的末尾,即排队等待。这种排队等待的方式可以保证请求资源的线程按照FIFO的顺序获得资源,避免了饥饿现象。当资源释放后,AQS会自动唤醒队列中的下一个线程,使其获得资源并继续执行。 需要注意的是,AQS的同步队列(Sync queue)是一个双向链表,包括头节点(head)和尾节点(tail),用于后续的调度。而条件队列(Condition queue)是一个单向链表,只有在使用Condition时才会存在,并且可能会有多个条件队列。 总结一下,AQS实现阻塞队列的原理是通过CLH队列来实现的,当共享资源被占用时,请求资源的线程会被添加到CLH队列中排队等待。当资源释放后,AQS会自动唤醒队列中的下一个线程,使其获得资源并继续执行。同步队列用于后续的调度,而条件队列只在使用Condition时才会存在。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值