【消息队列项目】C++11 异步操作实现线程池
目录
一.std::future
在C++中,std::future是一个模板类,它提供了一种机制来获取异步操作的结果。这里我将以纯文字方式详细描述std::future的使用和相关概念。
首先,我们需要了解异步操作。异步操作是指那些可以在后台执行的操作,不会阻塞当前线程。当我们需要执行一个耗时操作(比如计算、文件读写、网络请求等)时,我们可以将其作为异步任务,这样主线程可以继续执行其他任务,直到需要异步任务的结果时再等待其完成。
std::future就是用来表示这个异步任务的结果。它并不存储结果,而是作为一个句柄,通过它可以获取异步任务的结果。当异步任务完成时,结果会被存储在一个共享状态中,std::future可以访问这个共享状态。
使用std::future的基本步骤:
-
启动异步任务:我们可以使用std::async、std::packaged_task或者std::promise来创建一个异步任务,并获取与这个任务关联的std::future对象。
-
在需要结果的地方,通过std::future对象获取结果。
1.1.std::async
1.1.1.通俗版介绍
你可以把 std::async 想象成一个帮你“外包任务”的聪明管家。
- 你:就是主线程,正在处理主要工作(比如做菜)。
- 任务:一件你想完成,但又不想自己停下来去做的耗时工作(比如去楼下买瓶酱油)。
- std::async 管家:你把这个买酱油的任务“外包”给了他。
- std::future 收据/承诺:管家不会立刻把酱油给你,而是给你一张“收据”或一个“承诺”。这张收据上写着“未来会有一瓶酱油”。这张收据就是 std::future 对象。
现在,关键来了:这个管家派谁去执行“买酱油”这个任务呢?这取决于你给他的指令,也就是 std::launch 启动策略。
三种“外包”策略(启动策略)
策略一:懒汉模式 - std::launch::deferred
- 管家行为:管家接过你的任务,说:“好的,我知道了。”但他立刻就把任务忘在脑后了,根本没派人出去。
- 实际执行时机:什么时候才执行呢?直到你拿着那张收据(future),主动对他说:“喂,我的酱油呢?我现在就要!”(即调用 get() 或 wait() 方法)。这时,管家才一拍脑袋:“哎呀!我还没派人去呢!”然后他会立刻、亲自、就在你面前(在当前调用 get() 的线程里)跑去把酱油买回来给你。
- 特点:根本没有创建新线程。任务是“延迟”执行的,并且是同步的(你必须停下来等他把事办完)。适合那些“可能不需要,但如果需要就马上做”的任务。
策略二:勤快模式 - std::launch::async
管家行为:管家接过任务,立刻转身雇佣了一个新的临时工(创建一个新线程),把钱和任务清单交给他说:“快去快回!”
实际执行时机:任务立刻就开始了。新线程(临时工)立刻出发去买酱油。
你的状态:你可以继续做你的主菜(主线程继续运行),完全不用等待。
取结果:当你需要酱油时(调用 get()),会有两种情况:
- 如果临时工已经回来了,你直接拿到酱油。
- 如果临时工还没回来,那你必须停下来等他(主线程被阻塞),直到他回来把酱油交给你。
特点:明确创建了新线程,任务是真正异步执行的。适合那些明确需要后台并发执行的任务。
策略三:自动模式 - std::launch::deferred | std::launch::async(默认策略)
管家行为:管家很精明。他没有立刻决定。他会根据一些内部因素(比如系统忙不忙、任务重不重等)自己偷偷做决定。
可能的结果:
- 他可能选择“懒汉模式”(deferred),先记下来,等你要的时候再做。
- 他也可能选择“勤快模式”(async),立刻派个新线程去做。
对你的影响:你无法预测任务会怎么执行! 这对程序的行为有重大影响:
- 如果你假设它是异步的(认为它在新线程运行了),而它实际是延迟的,那么当你第一次调用 get() 时,会意外地导致长时间等待,破坏你的并发计划。
- 如果你依赖它创建新线程(比如任务有线程局部变量),而它选择了延迟执行,那么这些依赖就会出错。
给初学者的建议:尽量不要使用这个默认的自动模式。明确指定你想要的策略(async 或 deferred),这样代码的行为才是清晰、可预测的。不确定的时候,问问自己:“这个任务我必须立刻开始做吗?”,如果答案是“是”,就用 async;如果“不一定,也许等会儿做也行”,就用 deferred。
一个生活化的比喻总结
想象你要写一封信并寄出去。
- std::launch::deferred:你把信和邮票放在桌上,继续工作。只有当邮差上门问你“有信要寄吗?”的时候(get()),你才停下工作,现场写信、贴邮票、交给他。
- std::launch::async:你立刻叫来一个跑腿小弟,把写好的信和钱给他,说:“去帮我寄了。”然后你继续工作。小弟立刻出发。等他回来问你要小费(get())时,你才需要理他。
- 默认策略:你可能把信放在桌上(打算等邮差),也可能立刻叫了跑腿小弟。但你记性不好,过一会儿就忘了自己是怎么安排的了,导致邮差来时你措手不及。
1.1.2.专业描述
首先,std::async 是C++11中提供的一个用于异步任务处理的工具。
它可以将一个可调用对象(比如函数、Lambda表达式、函数对象等)包装起来,并使其可能在新线程中运行,然后返回一个std::future对象,通过这个future对象可以获取异步任务的结果。
当我们使用std::async时,我们可以指定一个启动策略(launch policy),这个策略决定了任务何时以及如何运行。
-
std::launch::deferred(延迟启动):
如果我们使用这个策略,那么任务不会立即开始执行。相反,它会延迟到我们调用future的get()或wait()方法时才会执行。而且,任务会在调用get()或wait()的线程中执行,而不是在新线程中。也就是说,任务会同步执行,直到任务完成,get()或wait()才会返回。 -
std::launch::async(异步启动):
这个策略会立即开始执行任务,任务会在一个新创建的线程中运行。我们可以在主线程中继续做其他事情,然后在需要的时候通过future来获取任务的结果。注意,如果任务已经开始执行,那么即使我们不调用get(),它也会在后台运行,直到完成。 -
默认策略(std::launch::deferred | std::launch::async):
当我们不指定策略时,或者指定为两者组合时,由系统决定是立即启动一个新线程(类似async)还是延迟执行(类似deferred)。这取决于系统当时的资源状况和实现。因此,使用默认策略时,我们无法确定任务是会延迟执行还是异步执行。
1.1.3.示例
示例1:std::launch::deferred(延迟执行)
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
int compute(int x, int y) {
std::cout << "计算线程ID: " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
return x + y;
}
int main() {
std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
// 使用延迟策略
auto future = std::async(std::launch::deferred, compute, 10, 20);
std::cout << "主线程继续执行其他工作..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "主线程工作完成" << std::endl;
// 只有在这里才会真正执行compute函数
int result = future.get();
std::cout << "计算结果: " << result << std::endl;
return 0;
}

停顿1秒

停顿2秒

恰好说明了如果我们使用这个策略,那么任务不会立即开始执行。相反,它会延迟到我们调用future的get()或wait()方法时才会执行。
示例2:std::launch::async(异步执行)
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
int compute(int x, int y) {
std::cout << "计算线程ID: " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
return x * y;
}
int main() {
std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
// 使用异步策略
auto future = std::async(std::launch::async, compute, 10, 20);
std::cout << "主线程继续执行其他工作..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "主线程工作完成" << std::endl;
// 任务已经在执行,这里只是等待结果
int result = future.get();
std::cout << "计算结果: " << result << std::endl;
return 0;
}

可以看到,这里主线程和计算线程是同时存在的,也就是说这个策略会立即开始执行任务,任务会在一个新创建的线程中运行。

这里就是在等待计算线程返回结果了(等待事件不足2秒)

示例3——默认策略(std::launch::deferred | std::launch::async)
当我们不指定策略时,或者指定为两者组合时,由系统决定是立即启动一个新线程(类似async)还是延迟执行(类似deferred)。
auto future = std::async(compute, 10, 20);
完全等价于:
auto future = std::async(std::launch::async | std::launch::deferred, compute, 10, 20);
至于效果我就不演示了,没什么好说的,就是上面两种策略的任意一种!!
1.2.std::packaged_task
1.2.1.介绍
std::packaged_task也是一个将任务和future关联起来的工具,但是它的使用方式与std::async不同。你可以把std::packaged_task看作是一个包装了可调用对象(如函数、lambda表达式等)的包装器,并且它允许你获取一个与包装的任务相关联的std::future对象。
核心概念:将任务“打包”
想象一下,你有一个任务(比如一个函数),你想在未来的某个时候执行它,并且希望得到它的结果。
你可以使用std::packaged_task来打包这个任务,然后你可以把这个打包好的任务传递给其他线程,或者稍后执行。
同时,你可以通过这个打包的任务获取一个std::future对象,用于在将来获取任务的结果。
一个简单的比喻
假设你是一个公司的老板,你有一个任务(比如做一份财务报告)。你把这个任务写在一张任务卡上,然后把这张任务卡放进一个特殊的信封里(这个信封就是std::packaged_task)。
这个信封有一个特点:当你把任务卡放进去的时候,信封会给你一张收据(std::future),这张收据可以用来领取任务完成后的结果(财务报告)。
然后,你可以把这个信封交给一个员工(另一个线程)去执行,或者你可以选择在某个时候自己打开信封执行任务。
如何使用?
-
创建打包任务:你需要创建一个std::packaged_task对象,并指定它要包装的任务(函数、lambda等)。注意,模板参数是函数签名,例如
int(int, int)表示一个返回int并接受两个int参数的函数。 -
获取future:通过调用
get_future()方法,你可以获得一个与任务结果关联的std::future对象。这样,当任务执行完毕后,你就可以通过这个future来获取结果。 -
执行任务:std::packaged_task本身是一个可调用对象,你可以直接调用它(就像调用函数一样),或者将它传递给另一个线程,让那个线程去执行。注意,任务通常只会被执行一次。
-
获取结果:通过之前获得的std::future对象,你可以获取任务的结果。如果任务还没有执行完毕,调用get()会阻塞直到结果可用。
与std::async的区别
-
std::async:它直接创建了一个异步任务,并且返回一个future。你不需要显式地管理任务的执行,它可能会在新线程中执行,也可能在调用get()时同步执行(取决于启动策略)。
-
std::packaged_task:它只是将任务包装起来,并允许你获取一个future,但是任务的执行需要你自己来安排。你可以选择在哪个线程、什么时候执行这个任务。它更像是一个可以移动和传递的任务对象,并且与future关联。
一个生活化的例子
假设你周末要做两件事:
-
去超市买东西(主线程任务)。
-
计算家庭月度开销(一个计算任务,比较耗时)。
你可以这样安排:
-
你创建一个计算家庭月度开销的任务,并用std::packaged_task把它打包。然后你得到一个future,用于以后获取计算结果。
-
然后,你可以把这个打包的任务(packaged_task)交给你的另一半,让他/她在你有空的时候去计算(或者你可以自己稍后计算)。同时,你可以先去超市买东西。
-
当你从超市回来,你需要知道家庭月度开销的结果时,你可以通过之前得到的future来获取。如果另一半已经计算完了,你立刻得到结果;如果还没算完,你就得等他/她算完。
为什么使用std::packaged_task?
它提供了更多的灵活性。
你可以控制任务的执行时机和方式,而且可以将任务对象(packaged_task)在多个线程之间传递,而std::async则不能这样控制。
另外,std::packaged_task还可以用于线程池等更复杂的并发模式中。
1.2.2.示例
示例1:基础使用(在同一线程中执行)
#include <iostream>
#include <future>
#include <thread>
int add(int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
return a + b;
}
int main() {
// 1. 创建packaged_task,包装add函数
// 模板参数是函数签名:int(int, int)
std::packaged_task<int(int, int)> task(add);
// 2. 获取与任务结果关联的future
std::future<int> result = task.get_future();
// 3. 执行任务(直接调用)
// 可以直接调用,也可以在线程中执行
task(10, 20); // 在这里调用,当前线程会执行任务
// 4. 获取结果(如果任务未完成,会阻塞等待)
std::cout << "计算结果: " << result.get() << std::endl;
return 0;
}
我们直接编译运行

等待了1秒

示例2:在子线程中执行任务
我们可以将任务移交给其他线程!!!
#include <iostream>
#include <future>
#include <thread>
#include <memory>
int multiply(int x, int y) {
std::this_thread::sleep_for(std::chrono::seconds(2));
return x * y;
}
int main() {
// 使用shared_ptr包装packaged_task
auto taskPtr = std::make_shared<std::packaged_task<int()>>(
std::bind(multiply, 5, 6)
);
// 获取future
std::future<int> result = taskPtr->get_future();
// 创建一个线程来执行任务
// 使用lambda表达式捕获shared_ptr,这样任务可以安全地在线程中执行
std::thread worker([taskPtr]() {
// 调用packaged_task
(*taskPtr)();
});
// 主线程可以做其他事情
std::cout << "主线程正在做其他工作..." << std::endl;
// 当需要结果时,获取它(会阻塞直到结果可用)
std::cout << "计算结果: " << result.get() << std::endl;
// 等待工作线程结束
worker.join();
return 0;
}

等待2秒(注意这两秒是父进程在等待子进程退出)

注意:我们想要使用std::packaged_task来封装一个函数,然后通过std::future来获取结果。
但是,std::packaged_task不能直接传递给std::thread,因为std::packaged_task不可拷贝,而且它也不是一个函数对象(虽然它重载了(),但它的类型是类模板,不能直接作为线程函数)。
1.3.std::promise
1.3.1.介绍
首先,我们需要理解std::future和std::promise的基本概念:
-
std::future:这是一个模板类,它提供了一种访问异步操作结果的机制。我们可以通过它来获取一个异步任务(比如在另一个线程中运行的函数)的返回值。但是,这个返回值可能还没有准备好,所以std::future提供了等待(wait)或检查(通过valid()等方法)的功能,直到异步操作完成并设置了值。
-
std::promise:这也是一个模板类,它提供了一种存储值(或异常)的方式,这个值可以被同一个线程或其他线程中的std::future对象读取。换句话说,std::promise允许你手动设置一个值,然后与它关联的std::future就可以读取这个值。
现在,我们来看它们如何配合工作:
-
当我们创建一个std::promise对象时,它内部会有一个共享状态(shared state),这个共享状态可以存储一个值(或异常)。
-
通过调用std::promise的get_future()成员函数,我们可以获得一个与这个promise对象关联的std::future对象。
-
然后,我们可以在某个线程中运行一个任务,这个任务会计算一个值,然后通过调用std::promise的set_value()方法将值存储到共享状态中。
-
同时,在另一个线程(或同一个线程)中,我们可以通过std::future的get()方法来获取这个值。如果共享状态中还没有值,那么get()方法会阻塞,直到值被设置。
这样,我们就实现了一个线程向另一个线程传递数据的效果。
注意:一个std::promise对象只能关联一个std::future对象,并且只能设置一次值。如果多次设置,会抛出异常。
详细的工作流程
让我们跟着代码的典型生命周期走一遍:
步骤 1:创建配对
std::promise<int> prom; // 创建一个 promise 对象,用于将来产生一个 int 类型的值。
std::future<int> fut = prom.get_future(); // 从 promise 获取与之关联的 future 对象。
此时,共享状态被创建,但里面是“空”的。future 还没有就绪。
步骤 2:生产者设置值(让 future “就绪”)
在生产者线程(比如一个工作线程)中,你操作 promise:
// 生产者线程
void producer(std::promise<int>& prom) {
// ... 执行一些复杂的计算 ...
int result = 42;
prom.set_value(result); // 关键操作!
}
prom.set_value(result) 做了两件至关重要的事:
- 存储数据:将计算结果 42 写入到共享状态中。
- 通知就绪:将共享状态标记为“就绪”,并唤醒任何正在等待这个 future 的线程(比如消费者线程)。
步骤 3:消费者获取值
在消费者线程(比如主线程)中,你操作 future:
// 消费者线程
int main() {
// ... 创建 promise 和 future,启动生产者线程 ...
int x = fut.get(); // 关键操作!
std::cout << "Got value: " << x << std::endl; // 输出 42
}
fut.get() 也做了两件关键的事:
- 等待就绪:如果共享状态还没就绪(即生产者还没调用 set_value),那么 get() 会阻塞当前线程,直到就绪。
- 取回数据:一旦就绪,它就从共享状态中取出值(这里是 42),然后返回。
关键点:get() 只能调用一次。调用后,共享状态中的数据就被取走(移动)了,future 变为无效。
1.3.2.示例
示例1——基本用法
- 步骤1:创建一个std::promise对象。
- 步骤2:通过这个std::promise对象获取一个std::future对象。
- 步骤3:启动一个线程,将std::promise对象传递给这个线程(或者通过其他方式让线程可以访问到这个promise)。
- 步骤4:在这个线程中执行一些计算,然后通过set_value()方法将结果设置到promise中。
- 步骤5:在原来的线程中,通过future的get()方法获取结果。
示例代码:
#include <iostream>
#include <thread>
#include <future>
void compute(std::promise<int> prom) {
// 模拟一个耗时的计算
std::this_thread::sleep_for(std::chrono::seconds(1));
int result = 42;
prom.set_value(result); // 将结果设置到promise中
}
int main() {
std::promise<int> prom; // 步骤1:创建一个std::promise对象
std::future<int> fut = prom.get_future(); // 步骤2:获取对应的future
std::thread t(compute, std::move(prom)); // 步骤3:启动线程,并将promise移动给线程
// 步骤5:在主线程中通过future获取结果
int result = fut.get(); // 这会阻塞,直到compute线程设置了值
std::cout << "Result: " << result << std::endl;
t.join();
return 0;
}
直接编译运行

等待1秒后

二.C++11 线程池实现
2.1.前置C++基础知识
2.1.1.可变模板参数
可变模板参数(Variadic Templates)是C++11引入的特性,允许函数或类模板接受任意数量的模板参数。
// 语法解释:
// typename... Args - 这是一个模板参数包(template parameter pack)
// Args... args - 这是一个函数参数包(function parameter pack)
template <typename... Args>
void func(Args... args) {
// 函数体
}
我们这里使用可变参数,其实是为了给任务函数传递参数进去的
话不多说,我们直接看例子
#include <iostream>
// 1. 接受函数和参数包,调用该函数
template<typename Func, typename... Args>
void call_func(Func f, Args... args) {
f(args...); // 使用参数包展开调用函数
}
// 2. 示例函数1
void print_sum(int a, int b) {
std::cout << "Sum: " << a + b << std::endl;
}
// 3. 示例函数2
void print_three(int a, int b, int c) {
std::cout << "Three numbers: " << a << ", " << b << ", " << c << std::endl;
}
int main() {
// 例1: 调用两个参数的函数
call_func(print_sum, 10, 20); // 输出: Sum: 30
// 例2: 调用三个参数的函数
call_func(print_three, 1, 2, 3); // 输出: Three numbers: 1, 2, 3
return 0;
}

怎么样?还是很容易懂的吧
2.1.2.decltype
我们在使用函数模板时可能会遇到下面这种情况
template<class T1,class T2>
void A(T1 a,T2,b)
{
?type? c=a+b;//c的类型不确定啊
....
}
这个时候怎么办呢?
C++11引入了关键字decltype,用来推断类型
用法·如下
decltype(expression) var;
expression可以是下面这几种情况
1.没有用括号括起的标识符,var的类型和该标识符的类型相同,包括const等限定符
我们可以举个例子
int x;
decltype(x) y;//将y定义为和x是同一类型的变量
const int y=22;
decltype(y) c=99;//c为const int类型
2.如果expression是一个函数调用,则var的类型与函数返回类型一样
这个过程不会调用函数哦!!!
int A()
{
return 33;
}
int main()
{
decltype(A()) c;//c的类型是A()的返回类型int
}
3,如果expression是一个左值,则var的类型是指向其类型的引用,前提是必须是要用括号括起的标识符
int x=99;
decltype((x)) y=x;
//y的类型就是int&
4.如果和上面三种情况都不符合的话,var的类型和expression的类型相同
decltype(3+9) y;
//3+9为int型,所以y为int型
学会decltype,我们就可以解决上面那个问题了
template<class T1,class T2>
void A(T1 a,T2,b)
{
decltype(a+b) c=a+b;//c的类型就是a+b的类型了
....
}
我们还可能遇到下面这种问题
template<class T1,class T2>
?type? A(T1 a,T2 b)//不知道返回类型是什么
{
return a+b;
}
为了解决这个问题,C++引入了一种新语法
double A();
auto A()->double;
//这两种写法是等价的
这样子我们也可以解决这个问题了
template<class T1,class T2>
auto A(T1 a,T2 b)->decltype(a+b)
{
return a+b;
}
示例
现在我们就能对我们的模板函数进行进一步的加工,我们加上返回值
#include <iostream>
// 带返回值的示例
int multiply1(int a, int b) {
return a * b;
}
// 带返回值的示例
float multiply2(float a, float b) {
return a * b;
}
// 支持返回值的版本
template<typename Func, typename... Args>
auto call_with_return(Func f, Args... args) -> decltype(f(args...)) {
return f(args...); // 返回函数调用的结果
}
int main() {
// 例1: 调用带返回值的函数
int result1 = call_with_return(multiply1, 5, 6);
std::cout << "result1: " << result1 << std::endl;
// 例: 调用带返回值的函数
float result2 = call_with_return(multiply2, 3.55, 6.9);
std::cout << "result2: " << result2 << std::endl;
return 0;
}

2.1.3.std::forward
左值(lvalue)和右值(rvalue)
在C++中,每一个表达式都有两个属性:类型(type)和值类别(value category)。
值类别分为左值和右值(实际上还有更细的分类,但初学者可以先理解为两种)。
左值(lvalue):
- 指的是一个持久的内存位置,我们可以取地址。
- 通常,变量名、函数名、返回左值引用的函数调用、前置自增/自减运算符等都是左值。
- 简单来说,左值可以出现在赋值语句的左边(但并不是所有左值都可以被赋值,比如被const修饰的左值就不能被赋值)。
右值(rvalue):
- 通常是临时的、生命周期很短的值,我们无法取地址。
- 例如:字面量(除了字符串字面量,因为字符串字面量是左值,它存储在静态存储区)、临时对象、返回非引用类型的函数调用、算术运算结果等。
例子:
int a = 5; // a是左值,5是右值。
int b = a; // a是左值,它可以出现在赋值语句的右边,但a本身是左值,因为我们可以取地址&a。
左值引用(lvalue reference)
左值引用就是我们通常使用的引用,用&表示。它必须绑定到一个左值上。
例如:
int x = 10;
int &ref = x; // 正确,ref是x的别名,x是左值。
int &ref2 = 5; // 错误,5是右值,不能绑定到左值引用(除非是const左值引用,见后)。
注意:const左值引用可以绑定到右值,因为const引用保证了不会修改绑定的值,所以允许绑定到临时对象。
const int &ref3 = 5; // 正确,const左值引用可以绑定到右值。
右值引用(rvalue reference)
C++11引入了右值引用,用&&表示。它只能绑定到右值,不能绑定到左值(除非使用std::move,后面会讲)。
例如:
int &&rref = 10; // 正确,10是右值。
int x = 10;
int &&rref2 = x; // 错误,x是左值,不能绑定到右值引用。
右值引用的出现主要是为了支持移动语义(move semantics)和完美转发(perfect forwarding)。
为什么需要 std::forward?
std::forward 是 C++11 中引入的完美转发工具,主要解决参数转发过程中的值类别保持问题。
核心问题:为什么需要 std::forward?
考虑这个场景:
#include <iostream>
using namespace std;
void process(int& x)
{
cout << "左值引用" << endl;
}
void process(int&& x)
{
cout << "右值引用" << endl;
}
template<typename T>
void wrapper(T&& arg) {
// 这里 arg 是一个具名变量,总是左值
process(arg); // 丢失了右值属性!
}
int main() {
int x = 10;
wrapper(x); // 调用左值版本 √
wrapper(10); // 期望调用右值版本,但实际调用了左值版本 ×
}

std::forward 的解决方案
完美转发是指在函数模板中,将参数按照原来的类型转发给另一个函数。这通常用于泛型编程,保持参数的值类别(左值或右值)。
template<typename T>
void wrapper(T&& arg) {
// 将arg按照原来的类型转发给另一个函数
another_function(std::forward<T>(arg));
}
这里,T&&是一个通用引用(universal reference),它既可以绑定到左值,也可以绑定到右值。然后使用std::forward来保持参数原来的值类别。
在C++中,当你看到函数模板 template<typename T> void wrapper(T&& arg) 这样的声明时,这里的 T&& 并不是一个普通的右值引用,而是一个特殊的类型,叫做“转发引用”或“通用引用”。
要理解这一点,我们需要从C++模板的类型推导和引用折叠规则说起。
- 首先,普通的右值引用(比如 int&&)只能绑定到右值(临时对象、即将销毁的对象),不能绑定到左值(有名字的变量、持久对象)。这是一个硬性规定。
- 但模板中的 T&& 不同。这里的 T 是一个待推导的类型参数。当编译器看到这样的声明并进行类型推导时,它会根据传入的参数来决定 T 到底是什么类型。
规则是这样的:
- 如果你传入一个左值给 wrapper 函数,比如 wrapper(x),其中 x 是一个 int 类型的变量,那么编译器会把 T 推导为左值引用 int&。
- 如果你传入一个右值给 wrapper 函数,比如 wrapper(10) 或 wrapper(std::move(x)),那么编译器会把 T 推导为普通的非引用类型 int。
现在,我们有了一个看似矛盾的情况:T&& 中的 T 可能被推导为引用类型。这就引出了C++的“引用折叠”规则。
引用折叠规则是处理引用的引用的规则。在C++中,你通常不能直接声明引用的引用,但在模板推导和类型别名中可能会隐式产生。规则很简单:
- & & 折叠成 &(左值引用)
- & && 折叠成 &(左值引用)
- && & 折叠成 &(左值引用)
- && && 折叠成 &&(右值引用)
应用这个规则,让我们看看前面的例子:
- 当传入左值 x 时,T 被推导为 int&,所以 T&& 变成 int& &&,根据引用折叠规则,折叠成 int&。因此,arg 实际上是左值引用。
- 当传入右值 10 时,T 被推导为 int,所以 T&& 就是 int&&。因此,arg 是右值引用。
这就是为什么 T&& 被称为“转发引用”或“通用引用”——它能接受左值和右值,并推导出相应的类型。
但这里还有一个问题:在函数内部,即使 arg 被推导为右值引用类型,它本身作为一个有名字的变量,实际上是一个左值。如果你直接传递 arg 给另一个函数,它会被当作左值处理。
这就是需要 std::forward<T> 的原因。std::forward 是一个条件转换:
- 如果 T 是左值引用类型(意味着原始传入的是左值),std::forward<T>(arg) 返回左值引用。
- 如果 T 是右值引用类型(意味着原始传入的是右值),std::forward<T>(arg) 返回右值引用。
换句话说,std::forward 的任务是保持参数的原始值类别:如果参数最初是左值,就保持为左值;如果最初是右值,就保持为右值。
所以,在 another_function(std::forward<T>(arg)) 这行代码中:
- 如果 wrapper 被调用时传入的是左值,arg 是左值引用,std::forward 将其作为左值传递给 another_function。
- 如果 wrapper 被调用时传入的是右值,arg 是右值引用但本身是左值,std::forward 将其转换为右值引用(实际上是右值),然后传递给 another_function。
这种技术被称为“完美转发”——完美地将参数按原样转发给另一个函数,包括它的值类别(左值/右值)、const限定符等所有属性。
完整示例
#include <iostream>
#include <utility>
// 1. 处理左值引用的函数
void process(int& x) {
std::cout << "处理左值引用: " << x << std::endl;
}
// 2. 处理右值引用的函数
void process(int&& x) {
std::cout << "处理右值引用: " << x << std::endl;
}
// 3. 处理常量的版本
void process(const int& x) {
std::cout << "处理常量引用: " << x << std::endl;
}
// 4. 普通转发(错误示例)
template<typename T>
void bad_wrapper(T&& arg) {
process(arg); // arg 总是被当作左值
}
// 5. 完美转发(正确示例)
template<typename T>
void good_wrapper(T&& arg) {
process(std::forward<T>(arg)); // 保持值类别
}
int main() {
std::cout << "=== 错误示例 ===" << std::endl;
int a = 10;
const int b = 20;
bad_wrapper(a); // 输出: 处理左值引用: 10
bad_wrapper(b); // 输出: 处理常量引用: 20
bad_wrapper(30); // 期望调用右值版本,但输出: 处理左值引用: 30
bad_wrapper(std::move(a)); // 输出: 处理左值引用: 10
std::cout << "\n=== 正确示例 ===" << std::endl;
good_wrapper(a); // 输出: 处理左值引用: 10
good_wrapper(b); // 输出: 处理常量引用: 20
good_wrapper(30); // 输出: 处理右值引用: 30 ✓
good_wrapper(std::move(a)); // 输出: 处理右值引用: 10 ✓
return 0;
}

怎么样?是不是保留了这些属性!!
2.1.4.可变模板参数和std::forward结合
#include <iostream>
#include <utility>
// 模拟一个接受多个参数的函数
void process(int a, double b, const std::string& c) {
std::cout << "处理: " << a << ", " << b << ", " << c << std::endl;
}
// 通用包装器
template<typename... Args>
void wrapper(Args&&... args) {
// 将所有参数完美转发给process
process(std::forward<Args>(args)...);
}
int main() {
std::string str = "hello";
wrapper(1, 3.14, "world"); // 右值转发
wrapper(2, 2.71, str); // str是左值,以左值引用转发
}
这个其实很简单
2.2.线程池实现
基于线程池执行任务的时候,入口函数内部执行逻辑是固定的,因此选择 std::packaged_task 加上 std::future 的组合来实现。
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
/* 实现一个能够获取任务返回值的线程池 */
class ThreadPool
{
public:
// 定义任务类型为无参数无返回值的函数对象
//也就是给函数类型void() 起别名为Functor
using Functor = std::function<void()>;
// 构造函数,创建指定数量的工作线程
ThreadPool(size_t tcount = 1) : _stop(false)
{
// 循环创建指定数量的工作线程
for (int i = 0; i < tcount; i++)
{
// 每个线程执行entry函数,this指针传递当前对象
_threads.emplace_back(&ThreadPool::entry, this);
}
}
// 析构函数,确保线程池正确停止
~ThreadPool()
{
stop();
}
// 向线程池提交任务,支持任意类型函数和参数
//push函数内部,会将这个传入的函数封装成一个异步任务(packaged_task),抛入到std::vector<Functor> _tasks; 任务队列里面
// 返回std::future对象,这个std::future对象是模板类,然后就通过这个std::future对象来获取任务的结果
template <typename F, typename... Args>
auto push(F &&func, Args &&...args) -> std::future<decltype(func(args...))>
{
// 推导传入函数的返回值类型
using return_type = decltype(func(args...));
// 使用std::bind将函数和参数绑定,创建无参可调用对象
//这个时候调用nfunc()就等价于调用func(args...)
auto nfunc = std::bind(std::forward<F>(func),std::forward<Args>(args)...);
//std::packaged_task不能直接传递给std::thread,因为std::packaged_task不可拷贝,而且它也不是一个函数对象
//所以把它放到智能指针里面去
auto task = std::make_shared<std::packaged_task<return_type()>>(nfunc);
// 在移动packaged_task之前获取future对象
std::future<return_type> ret = task->get_future();
//
{
// 加锁保护任务队列
std::unique_lock<std::mutex> lock(_mutex);
// 如果线程池未停止,将任务加入队列
if (!_stop)
{
//将构造出来的任务放到任务队列里面(使用lambda表达式)
_tasks.push_back([task]() {
(*task)();//(*task)()本质是可调用对象
});
// 通知一个等待的工作线程有新任务
_cond.notify_one();
}
}
// 返回future对象,调用者可以通过它获取任务执行结果
return ret;
}
// 停止线程池,等待所有工作线程完成
void stop()
{
if(_stop==true) return;
{
// 加锁设置停止标志
std::unique_lock<std::mutex> lock(_mutex);
_stop = true;
}
// 通知所有等待的工作线程
_cond.notify_all();//唤醒所有工作线程
// 等待所有工作线程结束
for (auto &thread : _threads)
{
if (thread.joinable())
thread.join();
}
}
private:
// 工作线程的入口函数
void entry()
{
while (!_stop)
{
std::vector<Functor> tmp_taskpool;
{
// 加锁访问任务队列
std::unique_lock<std::mutex> lock(_mutex);
// 如果线程池已停止且任务队列为空,退出循环
if (_stop && _tasks.empty())
break;
// 等待条件:线程池停止或有新任务
// wait会在条件满足时自动释放锁,被唤醒后重新获取锁
_cond.wait(lock, [this]
{ return this->_stop || !this->_tasks.empty(); });
// 交换任务队列,减少锁的持有时间
// 这样可以一次性取出所有待处理任务,在锁外执行
tmp_taskpool.swap(_tasks);
}
// 执行所有取出的任务
for (auto &task : tmp_taskpool)
{
task();//运行任何队列里面的函数
}
}
}
private:
std::atomic<bool> _stop; // 线程池停止标志,原子操作保证线程安全
std::mutex _mutex; // 互斥锁,保护任务队列和停止标志
std::condition_variable _cond; // 条件变量,用于线程间同步
std::vector<std::thread> _threads; // 工作线程池
std::vector<Functor> _tasks; // 任务队列,存储待执行的任务
};
我们去写一个测试案例
int Add(int num1,int num2)
{
return num1+num2;
}
int main()
{
ThreadPool pool(4);
for(int i=0;i<10;i++)
{
std::future<int> fu=pool.push(Add,11,i);
std::cout<<fu.get()<<std::endl;
}
pool.stop();
}

没有问题啊!!
更多推荐
所有评论(0)