使用C++11的Thread和condition_variable实现单例模式的线程池

28 篇文章 0 订阅
17 篇文章 0 订阅

线程池的结构

  首先我们来看一下线程池的大致结构,这里先不实现单例模式,线程池使用了模板类,可以用于任意类型的任务对象,其中有一些成员变脸,_task_queue用于保存线程池中要执行的任务,_count表示线程的数量,_isStop表示线程池是否要停止运行,停止线程执行任务,还有一个互斥锁用于保证线程对任务队列的访问时安全的,条件变量用于当任务队列为空时,让线程等待;当外部PushTask将任务加入到任务队列中时,唤醒线程来执行任务。

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <condition_variable>
#include <cassert>
#include <functional>
#include "task.hpp"

const int MAX_COUNT = 100;
template <class T>
class ThreadPool
{
public:
    ThreadPool(int count) : _count(count);
    
    ~ThreadPool();

    void Routine();

    bool PushTask(T* task);

    bool IsEmpty();
    
private:
    std::queue<T*> _task_queue; // 任务队列
    int _count = 1;              // 线程数量
    bool _isStop = false;

    std::mutex _mtx; // 互斥锁,用于保证任务队列访问线程安全
    std::condition_variable _cond;
};

Routine方法

  1.Routine方法是线程需要执行的方法,首先我们需要通过isStop判断当前线程池是否已经结束运行,如果为真就不需要继续执行任务了。进入循环后我们就需要对任务队列进行操作,由于任务队列属于共享资源,所以我们需要加锁。
  这里加锁使用std::unique_lock,借助了RAII的设计思想,在析构的时候自动解锁,防止我们自己忘记释放锁。我们要使用c++11中的condition_vairable条件变量,调用wait方法的时候需要传入一把该类型的锁,所以我们这里选择unique_lock来加锁。
  2.接着我们需要判断当前任务队列是否为空,如果为空就需要让线程在条件变量下进行等待,这里注意必须使用while循环,不能使用if,因为可能出现线程被伪唤醒的情况,比如wait调用失败(导致的结果就是线程看起来被伪唤醒),或者由于系统的一些原因导致线程被误唤醒了。如果使用if的话,线程被唤醒后,即使任务队列中没有任务,代码也会往下执行。所以需要使用while循环,让线程被唤醒以后重新判断一下任务是否为空,如果为空就继续阻塞。
  在判断条件中还需要加入一个!_isStop来判断当前线程被唤醒的时候,是否是线程池是否是停止了,因为我们在析构函数中会将_isStop的值设置为true,此时线程池中的任务队列为空,但是线程应该退出循环并结束运行,所以加上此判断条件。
  3.最后就是从队列中把任务拿出来,然后就可以解锁了,这里我们可以调用unique_lock的unlock方法手动解锁,因为到这里对共享资源的访问就结束了,后续执行任务不需要加锁。
tips:任务队列中的任务是T类型的指针,如果任务执行完就不需要的话,我们可以在这里delete释放任务对象占用的空间,当然也可以由主线程去释放。

void Routine()
{
    while (!_isStop)
    {
        std::unique_lock<std::mutex> locker(_mtx);
        while (IsEmpty() && !_isStop) // 注意这里要判断isStop,如果为真表示线程池要停止工作,就不要等待了
        {
            _cond.wait(locker);
        }
        //两种情况,一种是队列不空了,一种是线程池析构,线程被唤醒

        //如果是被唤醒,直接退出,不能取队列中再取元素了,因为已经没有元素了。
        if(_isStop)
        {
            break;
        }
        //队列不空,继续往下执行
        T task = _task_queue.front();
        _task_queue.pop();
        locker.unlock();
        task();
    }
}

PushTask

  该方法传入一个任务对象T的指针,由于是对临界资源,即任务队列的访问,所以同理我们需要上锁。将任务push到任务队列后,我们调用条件变量中的notify_one方法,随机唤醒一个线程来去任务队列中取任务执行。

bool PushTask(T *task)
{
    std::unique_lock<std::mutex> locker(_mtx);
    _task_queue.push(task);
    locker.unlock();

    _cond.notify_one();
    return true;
}

构造函数

  构造函数需要传入count参数初始化线程池中线程的数量,然后循环创建线程即可。这里通过thread类传递参数有多种方式,由于thread类使用了可变参数列表,所以可以直接传参,也可以使用std::bind函数,将参数和对应函数绑定在一起传过去,这里也可以把this指针转换成void*传过去,在线程执行函数中强转回ThreadPool类型的指针。我这里创建线程后直接调用了detach方法,线程执行完工作方法后会自动退出并释放资源,当然这里也可以把线程对象保存起来,最后调用join函数,让主线程来等待线程执行完,然后释放资源。

ThreadPool(int count) : _count(count)
{
   assert(count >= 0 && count <= 100);
   for (int i = 0; i < count; i++)
   {
       // std::thread(&ThreadPool::Routine, this).detach();
       std::thread(std::bind(&ThreadPool::Routine, this)).detach();
       std::cout << "线程" << i + 1 << "创建成功" << std::endl;
   }
}

析构函数

  如果线程池对象被析构,那它里面的线程一般也要随之释放掉,这个工作就可以在析构函数中进行。我们只需要把_isStop变量改为true即可,然后调用条件变量的notify_all方法唤醒所有线程,这样线程就会退出循环,并结束释放资源。
tips:如果创建线程的时候没有detach,而是将线程对象保存下来了,可以在析构函数中调用join方法,让主线程帮来释放线程资源。

~ThreadPool()
{
    std::unique_lock<std::mutex> locker(_mtx);
    _isStop = true;
    _cond.notify_all();  //得加锁,万一现在线程正在获取到锁,并且马上要进入wait状态,我们不加锁就无法通知到该线程
}

实现单例模式

static ThreadPool *_instance;
static std::mutex _mtx_for_ins; // 互斥锁,用于实现单例
//.......
template <class T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;

template <class T>
std::mutex ThreadPool<T>::_mtx_for_ins;

  实现单例模式就很简单了,这里采用懒汉模式,优点是使用对象时,对象才创建,所以不会提前占用内存,内存占用小。创建一个静态的对象指针,一个静态的互斥锁,同时不要忘记在类外对其进行初始化。

  我们定义一个public的GetInstance方法,让外界来获取单例,这里使用双重判空的机制,外层判空主要是为了加快效率,如果已经实例化了线程池对象,则不需要去竞争锁然后再判断是否为空了。第二层判空是必须的,因为在多线程环境下,第一层判空和加锁之间,可能有另一个线程已经把对象new出来了,这个时候如果不判空会导致再new一个新的线程池对象,所以这里使用了双重判空的机制。

 static ThreadPool *GetInstance(int count)
 {
     if (_instance == nullptr)
     {
         _mtx_for_ins.lock();
         if (_instance == nullptr)
         {
             _instance = new ThreadPool(count);
         }
         _mtx_for_ins.unlock();
     }
     return _instance;
 }

实现单例模式的另一种方法

  C++11可以保证:在当前线程执行到需要初始化静态变量时,如果有其他线程正在初始化该变量,则阻塞当前线程,直到初始化完成为止。

  静态局部变量会在一个类中只会有一份,而且只能在当前方法内访问,而且这个变量在程序结束前一直存在,不会每次调用都重新生成,用来实现单例模式就很方便。

static ThreadPool *GetInstance(int count)
{
    static ThreadPool* _instance;
    return _instance;
}

完成代码

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <condition_variable>
#include <cassert>
#include <functional>
#include "task.hpp"

const int MAX_COUNT = 100;
template <class T>
class ThreadPool
{
public:
    ~ThreadPool()
    {
        {
            std::unique_lock<std::mutex> locker(_mtx);
            _isStop = true;
        }
        _cond.notify_all();
    }

    void Routine()
    {
        while (!_isStop)
        {
            std::unique_lock<std::mutex> locker(_mtx);
            while (IsEmpty() && !_isStop) // 注意这里要判断isStop,如果为真表示线程池要停止工作,就不要等待了
            {
                _cond.wait(locker);
            }

            T *task = _task_queue.front();
            _task_queue.pop();
            locker.unlock();
            if (task != nullptr) // 注意判空,如果是由于线程池对象释放导致线程池关闭,此时任务队列中是没有任务的,不判空会出现空指针异常
            {
                task->run();
            }

            delete (task);
        }
    }

    bool PushTask(T *task)
    {

        std::unique_lock<std::mutex> locker(_mtx);
        _task_queue.push(task);
        locker.unlock();

        _cond.notify_one();
        return true;
    }

    bool IsEmpty()
    {
        return _task_queue.empty();
    }

    static ThreadPool *GetInstance(int count)
    {
        if (_instance == nullptr)
        {
            _mtx_for_ins.lock();
            if (_instance == nullptr)
            {
                _instance = new ThreadPool(count);
            }
            _mtx_for_ins.unlock();
        }
        return _instance;
    }

    //c++11以后可以使用内部静态变量来实现单例模式,C++11可以保证:在当前线程执行到需要初始化静态变量时,如果有其他线程正在初始化该变量,则阻塞当前线程,直到初始化完成为止。
    // static ThreadPool *GetInstance(int count)
    // {
    //     static ThreadPool* _instance;
    //     return _instance;
    // }

private:
    ThreadPool(int count) : _count(count)
    {
        assert(count >= 0 && count <= 100);
        for (int i = 0; i < count; i++)
        {
            // std::thread(&ThreadPool::Routine, this).detach();
            std::thread(std::bind(&ThreadPool::Routine, this)).detach();
            std::cout << "线程" << i + 1 << "创建成功" << std::endl;
        }
    }

private:
    std::queue<T *> _task_queue; // 任务队列
    int _count = 1;              // 线程数量
    bool _isStop = false;

    static ThreadPool *_instance;
    static std::mutex _mtx_for_ins; // 互斥锁,用于实现单例

    std::mutex _mtx; // 互斥锁,用于保证任务队列访问线程安全
    std::condition_variable _cond;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;

template <class T>
std::mutex ThreadPool<T>::_mtx_for_ins;
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值