C++线程池学习 Day03

注:涉及std::bind,std::packaged_task,共享指针,锁,emplace与push的比较,完美转发的深入剖析

❓3个问题:

1️⃣enqueue函数如何实现?

2️⃣析构函数做了什么?

3️⃣整体分析:这几个函数是如何配合的?

一、enqueue函数

由第一天的分析,我们知道enqueue函数的功能就是将任务提交到任务队列,并且确保这个任务是一个返回值为void,参数列表为空的可调用对象。

 template<class F, class...Args>
    auto enqueue(F&& f, Args&&...args)
        -> std::future<typename std::result_of<F(Args...)>::type>;
// add new work item to the pool
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;
}

1️⃣using return_type = typename std::result_of<F(Args...)>::type

有了第一天的分析,我们知道typename std::result_of<F(Args...)>::type是为了在编译器推导出用户提交的函数F在用参数Args...调用后的返回类型是什么。假设返回值为T,那么T::type会被编译器认为是成员变量,因此要加上typename告诉编译器T::type是一个类型名

using return_type实际上是在起别名。这样我们就不用每次都写typename std::result_of<F(Args...)>::type这么一大长串了。

根据第一天的分析,我们要返回一个std::future给用户,让用户能够异步地获取他们提交的任务的执行结果。std::future是一个模板类,它需要知道它将来要获取的值的类型,所以我们需要创建一个std::future<return_type>对象。例如,用户提交的任务返回值为int,我们就需要返回一个std::future<int>;用户提交的任务返回值为void,我们就需要返回一个std::future<void>

所以这行代码就是为了能正确声明std::future<return_type>

一个具体例子:

int add(int a, int b) { return a + b; }
​
ThreadPool pool(4);
// 用户提交一个任务,希望得到结果
std::future<int> result = pool.enqueue(add, 2, 3);

由第一天分析可知,在enqueue函数模板实例化时:

F被推导为int (*)(int,int)

Args...被推导为(int,int)

std::result_of<F(Args...)>::type就变成了:std::result_of<int (*)(int,int)>::type

编译器会计算这个表达式,得到::type就是int

进而return_type就是int

所以enqueue函数的返回类型就是std::future<int>

需要注意的是,在C++17中,std::result_of已被弃用,在C++20中已被移除。现在使用std::invoke_result_t

using return_type = std::invoke_result_t<F, Args...>;

联想到昨天学习的std::function<void()>,它和模板enqueue函数都体现了多态性,但是它们属于不同种类的多态。

std::function<void()> -> 运行时多态

机制:基于继承、虚函数和动态绑定

特点:在运行时决定调用哪个具体的函数。对方究竟是什么对象我并不在乎,只要长得像void(),我就能用

代价:通常伴随一些运行时开销,如虚函数表查找、动态分配

模板 -> 编译时多态

机制:基于模板和编译期的代码生成

特点:编译时,编译器会为每一种被使用的类型组合生成一份特定的代码。类型信息在编译期就完全确定。对方究竟是什么类型,传进来我再判断,并为特定类型生成一份专属的代码

代价:可能导致代码膨胀,但几乎没有运行时开销

总结:std::function<void()>像通用的遥控器,模板函数enqueue像万能工厂

这行using return_type代码体现了编译时多态,它使得这个enqueue函数模板能够:

1.自动推导出用户提交任务的返回类型

2.根据这个类型,构造并返回一个正确类型的std::future对象

3.让线程池能够以一种类型安全的方式,统一处理并返回任何类型的任务

2️⃣std::bind\std::packaged_task\std::make_shared

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

先看内层:std::bind -参数绑定器

作用:将一个可调用对象与其参数绑定在一起,生成一个新的可调用对象

在线程池中的作用:将用户传入的函数f和参数args提前打包好,创建一个不需要再传参的可调用对象

举个例子:

// 假设用户调用pool.enqueue([](int a, int b){ return a+b; }, 2, 3)
​
// std::bind 会做这样的事:
auto bound_func = std::bind( [](int a, int b){ return a+b; }, 2, 3 );
​
// 现在 bound_func 是一个无参的可调用对象,调用 bound_func() 等价于调用 lambda(2, 3),返回 5

std::forward<F>(f)和std::forward<Args>(args)...是完美转发,我们在第一天已经分析过了,它可以确保传递的参数保持其原始的值类别

追问1:std::bind生成的新的可调用对象,其参数列表是否必然为空?

答:不一定。只不过我们在线程池项目中,让它变成了空的。

std::bind的核心机制是参数绑定和占位符。

如果我们为可调用对象的所有参数都提供了具体的值,那么std::bind生成的新可调用对象就是一个无参对象

auto f1 = std::bind(func, 1, 2); // 绑定了所有参数
f1(); // 调用时不需要再提供参数,等价于 func(1, 2)

如果我们使用占位符std::placeholders::_1 那么生成的可调用对象就需要在调用时提供这些参数

auto f2 = std::bind(func, std::placeholders::_1, 2); // 只绑定了第二个参数
f2(10); // 调用时需要提供第一个参数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值