C++线程池实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011388696/article/details/82555625

最近读了muduo的源码,看了一下其中线程池的是实现。其中互斥量、条件变量都是库里面自己封装的,正好现在C++标准库里面有对应的类,所以就改造了一下,补充了部分注释。同时总结了一下条件变量和锁的使用。代码如下:
ThreadPool.h

#pragma once

#include <deque>
#include <vector>
#include <string>
#include <thread>
#include <mutex>
#include <functional>
#include <condition_variable>

namespace muduo
{
    using namespace std;

    class CThreadPool
    {
    public:
        // 入参为空的任务类型
        typedef function<void()> CTask;

        // 构造及析构函数
        explicit CThreadPool(const string& nameArg = string("ThreadPool"));
        ~CThreadPool();

        // 设置任务队列的数量上限
        void setMaxQueueSize(int maxSize) { maxQueueSize_ = maxSize; }
        // 设置工作线程初次执行时的消息回调
        void setThreadInitCallback(const CTask& cb)
        {
            threadInitCallback_ = cb;
        }

        // 开启指定数量的工作线程
        void start(int numThreads);
        // 退出线程池执行,退出所有的工作线程
        void stop();

        // 线程池名称
        const string& name() const
        {
            return name_;
        }

        // 任务队列多少
        size_t queueSize() const;

        // 新任务入队,若队列已满,将堵塞接口调用线程
        void run(const CTask& f);

    private:
        // 禁止拷贝构造
        CThreadPool(const CThreadPool&) = delete;
        CThreadPool& operator=(const CThreadPool&) = delete;

        // 判断队列是否已满,可采用lambda表达式替代
        bool isFull() const;
        // 完成任务到线程的分发
        // 线程池对象维护了任务队列,工作线程在执行时需要从任务队列中获取工作任务,
        // 所以只能将线程的执行体与线程池对象的接口关联,完成工作任务获取;
        void runInThread();
        // 从任务队列中取任务执行
        CThreadPool::CTask take();

private:
        mutable mutex mutex_; // 任务队列的互斥锁
        condition_variable notEmpty_; // 当前队列为空的条件变量,工作线程取任务时空则等待;
        condition_variable notFull_;  // 队列已满的条件变量,任务如队时,队列满则等待;
        string name_; // 线程池名称
        CTask threadInitCallback_; // 工作线程初次执行的回调
        vector<thread*> threads_;  // 工作线程集
        deque<CTask> queue_;  // 任务队列
        size_t maxQueueSize_; //任务量上限
        bool running_; // 线程池是否在执行
    };
}

ThreadPool.cpp

#include "MyThreadPool.h"
#include <assert.h>
#include <sstream>
#include <iostream>

using namespace muduo;

CThreadPool::CThreadPool(const string& nameArg)
    :name_(nameArg),
    maxQueueSize_(0),
    running_(false)
{
}

CThreadPool::~CThreadPool()
{
    if (running_)
    {
        stop();
    }
}

void CThreadPool::start(int numThreads)
{
    assert(threads_.empty());
    running_ = true;
    threads_.reserve(numThreads);

    for (int i = 0; i < numThreads; ++i)
    {
        // 将线程的执行过程绑定到线程池的runInThread
        threads_.push_back(new thread(&CThreadPool::runInThread, this));
    }
    if (numThreads == 0)
    {
        threadInitCallback_();
    }
}

void CThreadPool::stop()
{
    {
        lock_guard<mutex> lock(mutex_);
        running_ = false;
        // 喚醒所有的工作線程
        notEmpty_.notify_all();
    }

    //等待所有执行线程退出
    for each (auto ptr_thread in threads_)
    {
        ptr_thread->join();
    }
}

size_t CThreadPool::queueSize() const
{
    lock_guard<mutex> lck(mutex_);
    return queue_.size();
}

void CThreadPool::run(const CTask& task)
{
    if (!task)
    {
        return;
    }

    if (threads_.empty())
    {
        // 当前可执行线程数为0,当前主线程子集执行这个任务;
        task();
    }
    else
    {
        unique_lock<mutex> lck(mutex_);
        notFull_.wait(lck, [&]() { return !isFull(); });

        // 将task入队,唤醒执行线程
        queue_.push_back(task);
        lck.unlock();
        notEmpty_.notify_one();
    }
}

CThreadPool::CTask CThreadPool::take()
{
    unique_lock<mutex> lock(mutex_);
    // 若线程池需要退出,也不需要再等待
    notEmpty_.wait(lock, [&](){ return !queue_.empty() || !running_; });

    CTask task;
    if (!queue_.empty())
    {
        task = queue_.front();
        queue_.pop_front();
        if (maxQueueSize_ > 0)
        {
            //条件变量的分发不需要对互斥量加锁,以免唤醒的线程再次进入wait状态
            lock.unlock();
            notFull_.notify_one();
        }
    }
    return task;
}

bool CThreadPool::isFull() const
{
    return maxQueueSize_ > 0 && queue_.size() >= maxQueueSize_;
}

void CThreadPool::runInThread()
{
    try
    {
        if (threadInitCallback_)
        {
            threadInitCallback_();
        }

        while (running_)
        {
            CTask task(take());
            if (task)
            {
                task();
            }
        }
    }
    catch (...)
    {
        fprintf(stderr, "unknown exception caught in CThreadPool %s\n", name_.c_str());
        throw; // rethrow
    }
}

// 测试代码
// 1. 执行函数定义
#define Func(index) \
void func_##index() \
{\
    ostringstream ss;\
    ss << "=========== thread: " << std::this_thread::get_id() << std::endl;\
    std::cout << ss.str() << std::endl;\
    std::cout.flush();\
}\

Func(1)
Func(2)

//2. 执行任务入队列
#define RUN_IN_POOL(index) \
do{ tp.run(std::function<void()>(&func_##index)); }while(0)

void main()
{
    CThreadPool tp;
    tp.start(2);
    tp.setMaxQueueSize(3);

    RUN_IN_POOL(1);
    RUN_IN_POOL(2);

    // 睡眠一下,否则立即调用stop,部分任务还未执行完
    Sleep(1000);
    tp.stop();
    system("pause");
}

上述实现存在几个问题:
1. task类为无参的function类型,与其他可执行对象相比,无法保存状态;不能适配需要入参的场景;
2. 所有的成员变量的读写都用同一个互斥量保护,在工作线程较多情况下,运行效率不高;
3. run函数调用,插入待执行任务时,若当前任务队列已满,会将调用线程挂起;此时若调用该线程GUI线程,将导致界面卡死。

几点总结:
1. 条件变量的使用
a. 对共享变量修改的执行顺序
1) 通过lock_guard对互斥量mutex加锁;
2) 加锁完成后对变量进行修改;
3) 调用notify_one或notify_all接口唤醒在std::condition_variable上等待的线程(对外发送消息通知时,不需要持有锁)。
为了能够正确通知条件变量,即便共享变量为原子变量,也必须在对mutex互斥量加锁情况下进行修改。
b. 在条件变量上等待的线程
1) 通过unique_lock对同一个mutex上进行加锁,已保护共享变量;
2) 执行wait、wait_for或者wait_until,此时自动释放mutex,将线程挂起;
3) 某条件变量发出通知后,将当前线程唤醒,并自动获取mutex。同时,必须对条件变量进行判断,以避免为虚假唤醒。
2. lock_guard和unique_lock的差别
lock_guard和unique_lock都能实现互斥量的加锁操作。但unique_lock在guard_lock上做了扩展,支持锁状态的转移,锁状态的自动释放和获取。wait接口调用时,需要传入unique_lock类型的入参。

猜你喜欢

转载自blog.csdn.net/u011388696/article/details/82555625