C++多线程系列——分析&手写C++线程池

本文探讨了线程池在计算机编程中的作用,特别是在C++中的实现,涉及std::thread,std::mutex,std::future等并发编程工具,强调了线程池如何通过任务队列和预先创建的线程提高程序性能和降低延迟。
摘要由CSDN通过智能技术生成

在计算机编程中,线程池是一种为了让程序可以并发执行的软件设计模式,也叫作 replicated worker 或者 worker-crew model(为什么都带有worker呢?我觉得是因为可以线程池并不生产任务,只是执行任务,是卑微的打工人)。线程池中维护了多个等待 task 的线程,并且有一个保存待执行 task 的 task queue(复杂的线程池实现还会有 completed taskqueue),当 task 被线程池的使用者分配并加入到 task queue 时,等待的线程会执行这些任务。通过维护线程池中的线程,该 model 可以提高程序性能并降低程序执行的延迟(因为线程池中的线程是提前创建好的,在执行任务时不需要创建线程的时间开销)。线程池中的线程数量是与计算机资源相关的,特别是 CPU 资源。

The mental model

在这里插入图片描述
线程池一般要包含一个 task queue 和一些实际执行任务的线程,当然有些复杂的线程池包含一个 completed task queue(用于存放任务执行的结果)。

线程池的C++实现

  • 首先来分析需要用到哪些 C++ 技术:

    1. 既然是线程池肯定涉及到并发数据安全因此 std::mutexstd::unique_lock 是必不可少的。
    2. 在把 task 放入线程池时,放的肯定是一个可调用(callable)对象和其实参。C++11 通过 std::functionstd::bind 统一了可调用对象的各种操作,因此这两者也是必不可少的。
    3. 有些可调用对象有返回值,有些可调用对象没有返回值,为了使我们写的线程池更具有通用性,我们需要兼容这两种情况,在C++多线程编程中,返回值一般保存在 std::future 中,那么如何获得 std::future 并且和可调用对象关联起来呢?可以通过 std::packaged_task (关于用法可以参考 C++ 多线程基本操作——std::future | std::promise)。
  • 其次分析线程池需要暴露的接口:

    1. 创建线程池的构造函数(可以指定线程池中线程数量)
    2. 提交计算任务的函数(可以指定可调用对象以及可调用对象运行所需要的参数)
    3. 线程池停止的函数

下面是 github 上一个大佬基于 C++11 实现的 ThreadPool

#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>

class ThreadPool {
public:
    ThreadPool(size_t);
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args)
    	-> std::future<typename std::result_of<F(Args...)>::type>;
    ~ThreadPool();
private:
    // need to keep track of threads so we can join them
    std::vector< std::thread > workers;
    // the task queue
    std::queue< std::function<void()> > tasks;
    
    // synchronization
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads)
:   stop(false)
{
    for(size_t i = 0;i<threads;++i)
        workers.emplace_back(
            [this]
            {
                for(;;)
                    {
                        std::function<void()> task;

                        {
                            std::unique_lock<std::mutex> lock(this->queue_mutex);
                            this->condition.wait(lock,
                                [this]{ return this->stop || !this->tasks.empty(); });
                            if(this->stop && this->tasks.empty())
                                return;
                            task = std::move(this->tasks.front());
                            this->tasks.pop();
                        }

                        task();
                    }
            }
            );
}

// add new work item to the pool
/* 在 C++14 及以后可以删除尾序返回值即(-> 和后面的内容) */
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
    using return_type = typename std::result_of<F(Args...)>::type;

    auto task = std::make_shared< std::packaged_task<return_type()> >(
    	std::bind(std::forward<F>(f), std::forward<Args>(args)...)
    );

    std::future<return_type> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queue_mutex);

        // don't allow enqueueing after stopping the pool
        if(stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");

        tasks.emplace([task](){ (*task)(); });
    }
    condition.notify_one();
    return res;
}

// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for(std::thread &worker: workers)
        worker.join();
}

#endif

附录-知识补充

typename 的作用

  1. typename 用于引入一个模板参数,在模板定义语法中与 class 关键字的作用完全一样。
  2. 用于声明一个类型(因为当使用到作用域运算符::操作一个模板参数的类型成员时,编译器并不知道操作的是一个类型还是一个变量。)
    对于第一点无需说明,读者自然无比熟悉。但是对于第二点需要进行讲解。例如对于如下代码:
template<typename C>
void print2nd(const C& container){
    if(container.size() >= 2){
        C::const_iterator it(container.begin());
        ++it;
        int value = *it;  
        cout<<value;
    }
}

就会发生编译错误,因为编译器不知道 C::const_iterator 是个类型还是一个变量。C::const_iterator的解析有着逻辑上的矛盾: 直到确定了C是什么东西,编译器才会知道C::const_iterator是不是一个类型; 然而当模板被解析时,C还是不确定的。这时我们声明它为一个类型才能通过编译:

typename C::const_iterator it(container.begin());

std::result_of 的作用

std::result_of是一个模板类,它可以用于推导函数调用的结果类型。它的使用方式是std::result_of::type,其中F是函数类型,Args...是参数类型列表。然而,从C++17开始,std::result_of已经被弃用,取而代之的是std::invoke_result。在泛型编程中,std::result_of的一个常见用途是在编写模板函数时推导函数调用的返回类型。例如,考虑以下代码:

template<typename Func, typename Arg>
auto call(Func f, Arg a) -> typename std::result_of<Func(Arg)>::type {
    return f(a);
}

在这个例子中,std::result_of::type用于推导call函数的返回类型。这样,无论f函数的实际返回类型是什么,call函数都能正确地推导出它。

参考资料

typename 的两种用法
C++ 泛型编程 进阶篇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值