c++源码阅读__ThreadPool__正文阅读

一. 简介

本章我们开始阅读c++ git 高星开源项目ThreadPool, 这是一个纯c++的线程池项目, 并且代码量极小, 非常适合新手阅读

git地址: progschj / ThreadPool

二. 前提知识

为了面对不同读者对c++掌握情况不同的情况, 这里我会将基本上稍微值得一说的前提知识点, 全部专门写成一篇博客, 同学们在阅读本篇之前, 可以先去阅读前提知识部分
c++源码阅读__ThreadPool__前提基础部分

还有线程的一些基础知识
C++ 多线程 菜鸟教程

三. 源码:

因为源码时c++11的, 所以我们如果用最新的标准是跑不起来的, 所以这里我在下面源码部分把能用最新标准跑的版本的代码贴了出来

修改的地方只有一处, 如下

cpp 复制代码
返回类型的推导: 
typename std::result_of<F(Args...)>::type
改为了
typename std::invoke_result<F, Args...>::type

由于此项目比较小, 所以我们直接把代码全部贴出来, 并且在代码中, 用注释附上讲解

cpp 复制代码
#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>
#include <iostream>

class ThreadPool {
public:
    ThreadPool(size_t);
	// 给线程池设置任务的方法, 核心逻辑
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) -> std::future<typename std::invoke_result<F, Args...>::type>;
    ~ThreadPool();
private:
    // 线程队列的 vector
    std::vector< std::thread > workers;
    // 任务列表, 在前提知识里, 已经说明了, 通过lambda和bind将方法斗包装为void()类型的
    // 至于任务的返回值, 通过future实现
    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([](){}) 为什么能够直接生成Thread呢?
    	// 因为emplace_back 和 push_back不同, emplace_back传入的参数可以是 构造函数的 参数, 
    	// 所以这里写全了 应该是类似下面的代码
    	// workers.emplace_back( Thread( [](){} ) )
        workers.emplace_back(
            [this]
            {
                for (;;)
                {
                    std::function<void()> task;
                    {
                    	// 这里单独{}开一个域, 是因为unique_lock生效的范围是当前作用域
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        // 这里condition_variable的wait, 等待一个notify
                        this->condition.wait(lock, [this] { return this->stop || !this->tasks.empty(); });
                        if (this->stop && this->tasks.empty())
                        {
                            std::cout << std::this_thread::get_id() << std::endl;
                            return;
                        }
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }
                    // 脱离了lock域, 真正执行方法的地方, 还是多线程的, 如果写在上面的lock域里
                    // 那就变成 "单线程" 了
                    task();
                }
            }
        );
}


// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) ->std::future<typename std::invoke_result<F, Args...>::type>
{
    using return_type = typename std::invoke_result<F, Args...>::type;

    // 为什么要用shared_ptr? 因为后面使用queue.emplace, 会将task传递到queue中,
    // 当离开此方法时, task因为离开作用域, 会销毁, 而shared_ptr则不会销毁, 而是引用计数-1
    auto task = std::make_shared< std::packaged_task<return_type()> >(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)
    );
	// 并且这里要注意, task不是packaged_task, 而是shared_ptr
    std::future<return_type> res = task->get_future();
    {
        // 为什么这里要开一个单独的作用域呢? 因为这里unique_lock是以作用域进行lock的
        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");
		
		// 这里通过lambda, 及那个task包装为一个void()的函数, 里面的*task是shared_ptr指针指向的packaged_task
        tasks.emplace([task]() { (*task)(); });
    }
    // 任务装入queue后, 通知子线程执行
    condition.notify_one();
    return res;
}

// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
    {
    	// 同样的原因, unique_lock的生效范围是当前作用域
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    // 执行到notify_all的时候, 在线程的方法里, 实际上已经return了, 这里循环join
    // 是为了让主线程结束的时候, 子线程也随之结束
    for (std::thread& worker : workers)
        worker.join();
}

#endif

调用方法

cpp 复制代码
#include "ThreadPool.h"
using namespace std;


int main()
{
	// 线程数量为4的鲜橙汁
    ThreadPool pool(4);

	// 8个返回类型都是int的future数组
    std::vector< std::future<int> > results;
    for (int i = 0; i < 8; ++i) {
        auto f = [i]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            return i;
        };
        // 设置任务, 并将返回的future放入results里
        results.emplace_back(
            pool.enqueue(f)
        );
    }

	// 循环打印结果
    for (auto&& result : results)
        std::cout << result.get() << endl;

	// 设置一个string(const char*, const char*)的方法, 并获取返回的future<string>
    std::future<string> f = pool.enqueue([](const char* s1, const char* s2) {
        return string(s1) + s2;
    }, "hello ", "world");

    cout << f.get() << endl;
    std::cout << std::endl;

    return 0;
}

执行结果

总结

技巧:

  1. 通过lambda 或者 bind 来改变函数的参数个数
  2. 通过构造 packaged_task来改变返回值传递的方式, 方便将方法统一放入vector, 并且是异步执行的
  3. 通过lambda来改变函数返回值类型
相关推荐
东方巴黎~Sunsiny6 分钟前
java-图算法
java·开发语言·算法
ad禥思妙想2 小时前
如何运行python脚本
开发语言·python
Matlab程序猿小助手2 小时前
【MATLAB源码-第218期】基于matlab的北方苍鹰优化算法(NGO)无人机三维路径规划,输出做短路径图和适应度曲线.
开发语言·嵌入式硬件·算法·matlab·机器人·无人机
威威猫的栗子2 小时前
用 Python 与 Turtle 创作属于你的“冰墩墩”!
开发语言·python·turtle
qq_428639612 小时前
植物明星大乱斗15
c++·算法·游戏
IT古董2 小时前
【机器学习】超简明Python基础教程
开发语言·人工智能·python·机器学习
黑客Ash2 小时前
网络安全知识点
开发语言·php
童先生2 小时前
如何将java项目打包成docker 镜像并且可运行
java·开发语言·docker
feilieren2 小时前
SpringBoot 2.x 整合 Redis
java·开发语言·spring
晓看天色*2 小时前
[JAVA]MyBatis框架—获取SqlSession对象
java·开发语言·前端