注:涉及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); // 调用时需要提供第一个参数

最低0.47元/天 解锁文章
1862

被折叠的 条评论
为什么被折叠?



