目录
- 前言
- 1. 多线程
- 2. 问答环节
- 2.1 线程启动相关问题
- 2.2 线程启动函数参数相关问题
- 总结
前言
杜老师推出的 tensorRT从零起步高性能部署 课程,之前有看过一遍,但是没有做笔记,很多东西也忘了。这次重新撸一遍,顺便记记笔记。
本次课程学习 tensorRT 高级-学习深度学习中涉及的线程知识
课程大纲可看下面的思维导图
1. 多线程
这节课我们学习多线程知识
1. 这里的多线程主要指算法部署时所涉及的多线程内容,对于其他多线程知识需要自行补充
2. 常用线程组件有 thread、mutex、promise、future、condition_variable
3. 启动线程 thread 以及 join、joinable、detach、类函数为线程启动函数
我们先来看一个完整的启动线程案例,代码如下:
#include <thread>
#include <chrono>
#include <stdio.h>
using namespace std;
void worker(){
printf("hello thread.\n");
this_thread::sleep_for(chrono::milliseconds(1000));
printf("worker done.\n");
}
int main(){
// 完整的启动线程案例
// thread t(func, args...);
thread t(worker);
t.join(); // 等待线程结束
printf("done.\n");
return 0;
}
上述代码实现了一个简单的多线程程序,它通过启动一个新线程,在新线程中执行 worker 函数,该函数首先打印 “hello thread.”,然后暂停 1 秒钟,最后再打印 “worker done.”。同时,主线程会等待新线程执行完成后继续执行,并在最后打印 “done.”。因此,整个程序在主线程和新线程并行执行,通过多线程的方式实现了同时输出 “hello thread.” 和 “worker done.”,并在最后输出 “done.”。
执行效果如下:
我们来考虑下几个问题,首先就是 join 必须加吗?不加会怎样呢?
我们不加 join 的执行效果如下:
可以看到程序在 return 0; 的时候直接崩溃了,因此我们得到第一个规则,即
t.join 如果不加,会在析构时提示异常,出现 core dumped,因此只要线程 t 启动了,就必须要 join,没有启动则不必要 join
如果我们的线程 t 没有启动,但是我们执行了 join,会发生什么呢?
我们不启动线程,但是执行 t.join,执行效果如下:
可以看到程序崩溃了,因此引申出第二点,即
若 t 没有启动线程,去执行 t.join 也会异常,因此只要线程 t 没有启动,一定不能 join
这样做看起来有点死板吖,有没有方法可以添加线程启动呢?那这个时候就需要使用到 joinable(),代码如下:
#include <thread>
#include <chrono>
#include <stdio.h>
using namespace std;
void worker(){
printf("hello thread.\n");
this_thread::sleep_for(chrono::milliseconds(1000));
printf("worker done.\n");
}
int main(){
// 完整的启动线程案例
// thread t(func, args...);
thread t; //(worker);
if (t.joinable()) // 如果它可以 join, 那就 join
t.join(); // 等待线程结束
printf("done.\n");
return 0;
}
这是我们写线程销毁的时候常用的方法,这个方法的优点在于无论你的线程是否启动,在析构或者说线程结束时都可以正常退出
我们理解完了 join、joinable 之后,我们再来理解下 deteach,代码如下:
#include <thread>
#include <chrono>
#include <stdio.h>
using namespace std;
void worker(){
printf("hello thread.\n");
this_thread::sleep_for(chrono::milliseconds(1000));
printf("worker done.\n");
}
int main(){
// 完整的启动线程案例
// thread t(func, args...);
thread t(worker);
t.detach();
// if (t.joinable()) // 如果它可以 join, 那就 join
// t.join(); // 等待线程结束
printf("done.\n");
return 0;
}
上述代码通过 detach() 函数将新线程与主线程分离,使得主线程不再等待新线程的完成,并且在主线程和新线程并行执行的情况下,直接输出 “done.”。需要注意的是,分离后的线程在执行完毕后会自动释放资源,不再需要显式调用join() 函数。
t.detach() 是分离线程,取消管理权,使得线程成为野线程,一般不建议使用,但有时候又不得不使用
野线程是不需要 join 的,线程交给系统管理,程序退出后,所有线程才退出
执行效果如下:
我们可以看到 hello 并没有打印出来,难道线程没执行吗?其实是因为主线程程序执行太快了,detach 将子线程与主线程分离,使得主线程不再等待子线程的完成,因此主线程还没等子线程打印内容时就已经结束了,所以 hello thread 没有打印出来
如果我们想看到子线程的打印结果,我们可以在主线程中等待一段时间,代码如下:
#include <thread>
#include <chrono>
#include <stdio.h>
using namespace std;
void worker(){
printf("hello thread.\n");
this_thread::sleep_for(chrono::milliseconds(1000));
printf("worker done.\n");
}
int main(){
// 完整的启动线程案例
// thread t(func, args...);
thread t(worker);
t.detach();
// if (t.joinable()) // 如果它可以 join, 那就 join
// t.join(); // 等待线程结束
this_thread::sleep_for(chrono::milliseconds(2000));
printf("done.\n");
return 0;
}
执行效果如下:
了解完线程启动的相关知识后,我们来看下线程启动函数的参数是怎么传递的,示例代码如下所示:
#include <thread>
#include <chrono>
#include <stdio.h>
using namespace std;
void worker(int a){
printf("hello thread. %d\n", a);
this_thread::sleep_for(chrono::milliseconds(1000));
printf("worker done.\n");
}
int main(){
// 完整的启动线程案例
// thread t(func, args...);
thread t(worker, 666);
t.join();
// t.detach();
// if (t.joinable()) // 如果它可以 join, 那就 join
// t.join(); // 等待线程结束
// this_thread::sleep_for(chrono::milliseconds(2000));
printf("done.\n");
return 0;
}
运行效果如下:
可以看到传递的变量 a 被正常打印了,说明我们参数传递没有问题
那我们有时候不想要传值,而是想要传引用该如何处理呢?代码如下如下所示
#include <thread>
#include <chrono>
#include <iostream>
#include <stdio.h>
using namespace std;
void worker(int a, string& str){
printf("hello thread. %d\n", a);
this_thread::sleep_for(chrono::milliseconds(1000));
printf("worker done.\n");
str = "output";
}
int main(){
// 完整的启动线程案例
// thread t(func, args...);
string param;
thread t(worker, 666, std::ref(param));
t.join();
// t.detach();
cout << param << endl;
// if (t.joinable()) // 如果它可以 join, 那就 join
// t.join(); // 等待线程结束
// this_thread::sleep_for(chrono::milliseconds(2000));
printf("done.\n");
return 0;
}
运行效果如下:
可以看到 reference string 被正常打印出来了,因此你如果要在线程函数中传引用,一定要用 std::ref() 的方式去传递,传完之后就可以去访问和修改,还有一个细节,传进去的参数是存在生命周期的,如果在线程启动函数在执行的过程中,引用参数已经销毁的话,则程序会发生崩溃,这是关于线程启动函数传参的相关知识
接下来我们来看下类成员函数作为线程启动函数的使用,我们不难写出下面的示例代码:
class Infer{
public:
Infer(){
worker_thread_ = thread(infer_worker);
}
private:
void infer_worker(){
}
private:
thread worker_thread_;
};
运行效果如下:
可以看到报错了,它提供说 void Infer::infer_worker() 是非静态成员函数,难道我需要写成静态成员函数吗?似乎可行,但是这么做有一个很大的缺陷,静态函数没有办法访问 Infer 指针,你也可以将 Infer 传进来,修改后的代码如下所示:
class Infer{
public:
Infer(){
worker_thread_ = thread(infer_worker, this);
}
private:
static void infer_worker(Infer* self){
}
private:
thread worker_thread_;
}
这么做似乎能解决问题,但是有点过于繁琐了,我们一般不这么做,我们采用的方式如下所示:
class Infer{
public:
Infer(){
// worker_thread_ = thread(infer_worker, this);
worker_thread_ = thread(&Infer::infer_worker, this);
}
private:
// static void infer_worker(Infer* self){
// }
void infer_worker(){
}
private:
thread worker_thread_;
}
在类构造函数中创建线程时,我们使用的是类成员函数的指针,而不是直接调用成员函数
2. 问答环节
博主对多线程相关的知识不怎么了解,因此疯狂询问 chatGPT,故此做个记录方便下次查看,以下内容来自于博主和 chatGPT 之间的对话
2.1 线程启动相关问题
问题1:如果我在创建完一个线程 t 以后执行 t.join() 启动该线程,则主线程是不是会一直阻塞等待子线程的执行完成?🤔
回答:是的,如果在主线程中执行 t.join(),主线程会一直阻塞,直到线程 t 执行完成才会继续执行主线程的后续代码。
调用 t.join() 的作用是等待线程 t 执行完成,确保在主线程退出之前,子线程能够正确地执行完毕。这样可以避免主线程在子线程还没有执行完毕的情况下退出,从而确保线程的正确执行。
如果在主线程中没有调用 t.join(),而是执行 t.detach() 让线程 t 分离,主线程会立即继续执行后续的代码,而不会等待子线程执行完成。这样可能会导致子线程在主线程退出后继续执行,可能会引发一些问题。
总的来说,如果希望主线程等待子线程执行完成,可以在主线程中调用 t.join();如果希望主线程和子线程并行执行,可以使用 t.detach() 将子线程分离。但是在分离线程后,就无法再使用 join() 来等待子线程执行完成了。
问题2:我还有个疑问,线程 t 创建后执行 t.detach 让子线程分离出去,那子线程是算启动了还是没有启动呢?我需不需要执行 t.join 启动线程呢?🤔
回答:当你调用 t.detach() 将线程 t 分离后,线程是启动了的,但是主线程和子线程是独立运行的,它们的执行顺序和具体运行时间是无法确定的。线程的分离意味着主线程和子线程的执行完全独立,主线程不会等待子线程结束,也无法获取子线程的状态和执行结果。
如果你在主线程中调用了 t.detach(),那么就不需要再调用 t.join()。t.detach() 分离了线程 t,意味着主线程和子线程之间不再有关联,主线程不需要等待子线程结束,主线程可以继续执行自己的任务,而子线程也可以独立运行。如果在分离后再调用 t.join(),将会引发 std::system_error 异常。
问题3:那如果按照这种方式来说的话,等待子线程其实就是在等待一个函数执行完成吗?那为什么还要另外启动一个线程呢?直接调用一个函数不就行了吗?反正都是串行,要等待函数执行结束,那线程这么使用是不是没有任何意义呢?正常的多线程该如何使用?是不是都是使用 t.detach() 函数?🤔
回答:等待子线程执行完成的确类似于等待一个函数执行完成,但多线程的使用场景主要是为了实现并发执行,充分利用多核处理器的能力,提高程序的性能和响应性。
使用多线程的主要目的是将计算密集型或耗时的任务分配给多个线程并行执行,从而加快任务的完成速度。例如在图像处理、数据处理、网络通信等场景中,可以将不同的任务分配给不同的线程同时执行,从而提高整体的处理效率。
另外,多线程还可以用于实现异步编程,即主线程在执行任务的同时,可以将某些耗时的操作交给子线程来处理,而不需要等待子线程执行完成。这样可以提高程序的响应速度,使得用户在主线程上执行操作时,不会感觉到程序的卡顿。
总的来说,多线程的使用主要是为了充分利用多核处理器的能力,提高程序的并发性和响应性,从而提高程序的性能和用户体验。
关于线程的使用,t.detach() 是一种方式,用于将线程分离,使得主线程和子线程并行执行,但需要注意的是,分离后的线程无法通过 t.join() 来等待执行完成,因此需要确保线程的执行不会对主线程产生影响。
另一种方式是通过 t.join() 来等待子线程执行完成,这样可以确保在主线程退出之前,子线程能够正确地执行完毕。但这种方式需要主线程等待子线程执行完成,可能会影响程序的响应性。
综合考虑,正确的多线程使用方式取决于具体的场景和需求。在一些情况下,可以使用 t.detach() 将任务分离出去并行执行,而在一些需要等待子线程执行结果的情况下,可以使用 t.join() 来等待子线程执行完成。同时,也需要注意合理地控制线程的数量和任务的分配,避免线程竞争和资源浪费。
问题4:在深度学习模型部署过程中,哪些任务是计算密集型或耗时操作,适合使用多线程,哪些任务是简单的计算或IO操作,不适合使用多线程,你能举例说明吗?🤔
回答:在深度学习模型部署过程中,通常涉及到以下几个主要任务:
1. 模型推理(Inference):将输入数据送入已经训练好的深度学习模型中,进行预测或特征提取。这是一个计算密集型的任务,适合使用多线程来并行处理多个推理任务,充分利用多核 CPU 或 GPU 的计算能力。例如,对于图像分类模型,可以同时处理多张图片的分类任务。
2. 数据预处理:将原始数据进行转换、归一化、裁剪等预处理操作,以便输入到深度学习模型中。这通常是一个计算密集型和 IO 密集型的任务,适合使用多线程来并行处理多个数据样本。例如,在图像分类任务中,可以将多个图像同时进行裁剪、缩放和归一化操作。
3. 模型加载和初始化:将训练好的深度学习模型加载到内存中,并进行初始化。这是一个IO密集型的任务,适合使用多线程来异步加载多个模型文件,以减少模型加载时间。
4. 模型保存和导出:将训练好的深度学习模型保存到磁盘或导出为其他格式。这也是一个 IO 密集型的任务,适合使用多线程来异步保存多个模型文件。
5. 数据后处理:将模型推理的结果进行解码、后处理和可视化等操作。这通常是一个计算密集型的任务,适合使用多线程来并行处理多个后处理任务。
需要注意的是,多线程并不是适合所有任务的解决方案。在设计多线程时,需要综合考虑任务的特点、硬件资源、线程间的数据依赖关系以及线程切换的开销等因素。对于一些简单的计算或 IO 操作,多线程可能会引入额外的开销,并不能提高性能,甚至可能导致性能下降。因此,在使用多线程时,需要进行合理的任务划分和线程调度,确保每个线程都能得到充分的利用,从而达到优化性能的目的。
2.2 线程启动函数参数相关问题
问题1:假设现在我创建了一个线程 t,并通过引用传递了一个参数 img,在线程 t 的启动函数内部对参数 img 做了一系列处理,但是我在创建完 t 以后直接执行了 t.detach,让主线程和子线程完全分离开了,现在我有个疑惑,我失去了线程 t 的控制权,我如何知道线程启动函数什么时候处理完 img 参数了呢?我又该如何拿到处理完成的结果呢?🤔
回答:当你在主线程中创建了一个线程,并通过引用传递了参数 img 给子线程 t,然后将 t 分离(t.detach()),主线程就会失去对 t 的控制权,无法直接获取 t 的运行状态或判断子线程是否完成。
在这种情况下,你可以考虑使用线程的共享数据和线程同步机制来解决问题。可以使用标志变量或互斥锁来实现线程之间的通信和同步。
一种常用的方法是使用 std::promise 和 std::future 来在主线程和子线程之间传递数据和同步。具体做法如下:
1. 在主线程中,使用 std::promise 对象创建一个 std::future 对象,并将这个 std::future 对象传递给子线程。
2. 子线程中在处理完参数 img 后,通过 std::promise 对象的 set_value 方法或者 set_exception 方法来设置结果或异常,并将结果传递给主线程。
3. 在主线程中,通过调用 std::future 对象的 get 方法来等待子线程的处理结果,如果子线程出现异常,可以通过 std::exception 来捕获异常。
以下是示例代码:
#include <iostream>
#include <thread>
#include <future>
// 子线程的启动函数
void processImage(std::promise<std::string>& promise, const std::string& img) {
// 在这里处理参数 img
// 假设处理后得到了结果 result
std::string result = "Processed " + img;
// 设置处理结果给主线程
promise.set_value(result);
}
int main() {
std::string img = "image.jpg";
// 创建 std::promise 和 std::future
std::promise<std::string> promise;
std::future<std::string> future = promise.get_future();
// 创建子线程并传递参数 img 和 std::promise
std::thread t(processImage, std::ref(promise), std::cref(img));
// 分离子线程
t.detach();
// 等待子线程的处理结果
try {
std::string result = future.get();
std::cout << "Result: " << result << std::endl;
} catch (const std::exception& ex) {
std::cout << "Exception occurred: " << ex.what() << std::endl;
}
return 0;
}
通过上述方法,你可以在主线程中等待子线程的处理结果,并且可以捕获子线程中抛出的异常。这样就能够知道子线程的处理是否完成,并获取到子线程处理的结果。
问题2:在你提到的使用 promise 和 future 来实现数据同步的过程中,你有提到过使用 promise 对象创建一个 future 对象,我对这句话并不理解,你能详细解释下吗? std::promise 和 std::future 分别是什么呢?🤔
回答:当我们想要在线程之间进行数据传递或同步时,可以使用 std::promise 和 std::future 这两个类模板。
- std::promise:是一个模板类,它允许在一个线程中产生一个值,并在另一个线程中获得这个值。我们可以将一个 std::promise 关联到一个 std::future,从而允许线程间进行数据的传递。
- std::future:也是一个模板类,它表示一个可能在某个时间点获得的值。通过关联到 std::promise,可以在一个线程中获取 std::promise 中设置的值。
使用 std::promise 和 std::future 的一般步骤如下:
1. 创建一个 std::promise 对象,它用于产生一个值,并与一个 std::future 关联。
2. 在一个线程中,通过 std::promise 设置值,也就是将需要传递的数据存放到 std::promise 中。
3. 在另一个线程中,通过 std::future 获取值,也就是从 std::promise 中取出被设置的数据。
通过这样的方式,我们可以在一个线程中生成数据,然后在另一个线程中获取这个数据,实现线程间的数据传递和同步。
示例代码如下:
#include <iostream>
#include <thread>
#include <future>
void producer(std::promise<int>& promise) {
// 模拟生成数据
int data = 42;
// 设置值到 promise 中
promise.set_value(data);
}
void consumer(std::future<int>& future) {
// 从 future 中获取值
int data = future.get();
std::cout << "Received data from producer: " << data << std::endl;
}
int main() {
// 创建 promise 和 future
std::promise<int> promise;
std::future<int> future = promise.get_future();
// 启动生产者和消费者线程
std::thread producerThread(producer, std::ref(promise));
std::thread consumerThread(consumer, std::ref(future));
// 等待线程完成
producerThread.join();
consumerThread.join();
return 0;
}
在这个示例中,producer 函数生成了一个值并设置到 promise 中,然后 consumer 函数从 future 中获取了这个值并进行处理。通过 std::promise 和 std::future,我们在两个线程间实现了数据的传递和同步。生产者和消费者模式应该是深度学习中常用的一种模式,生产者线程读取图片并完成预处理,消费者线程执行推理过程,主线程通过 promise 和 future 拿到推理结果,是不是这么回事呢?🤔
问题3:还有一点想问,future.get() 可以拿到子线程的结果,那么在你去拿结果的时候子线程还没有执行完成,这个时候是不是会等待子线程的执行呢?🤔
回答:是的,future.get() 方法会等待子线程执行完成并返回结果。如果在调用 future.get() 时,子线程还没有执行完成,主线程会被阻塞,直到子线程执行完成并返回结果。
这是因为 future.get() 是一个阻塞调用,它会一直等待,直到对应的 std::promise 设置了值或者子线程抛出了异常。一旦子线程设置了值或抛出了异常,future.get() 就会返回对应的结果或抛出异常,并且主线程继续执行。
如果你希望在获取结果时不阻塞主线程,你可以使用 std::future::wait_for() 方法来检查是否已经获取到结果。wait_for() 方法可以设置一个时间段,如果在指定时间内获取到了结果,它会立即返回;如果超过了指定时间仍未获取到结果,它也会返回。这样可以在主线程中做一些其他的操作,而不是一直等待子线程完成。
示例代码如下:
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
void producer(std::promise<int>& promise) {
// 模拟生成数据
int data = 42;
// 设置值到 promise 中
std::this_thread::sleep_for(std::chrono::seconds(3)); // 模拟耗时操作
promise.set_value(data);
}
int main() {
// 创建 promise 和 future
std::promise<int> promise;
std::future<int> future = promise.get_future();
// 启动生产者线程
std::thread producerThread(producer, std::ref(promise));
// 检查是否已经获取到结果,每隔1秒检查一次
std::future_status status;
do {
status = future.wait_for(std::chrono::seconds(1));
if (status == std::future_status::timeout) {
std::cout << "Waiting for result..." << std::endl;
}
} while (status != std::future_status::ready);
// 获取结果
int data = future.get();
std::cout << "Received data from producer: " << data << std::endl;
// 等待生产者线程完成
producerThread.join();
return 0;
}
在这个示例中,我们使用 future.wait_for() 方法每隔1秒检查一次是否已经获取到结果,如果在 1 秒内未获取到结果,就打印一条提示信息。当获取到结果后,主线程会继续执行。这样可以在主线程中做其他的操作,而不会一直等待子线程完成。
问题4:在你之前提供的示例代码中,我有注意到在线程启动函数的参数传递过程中 std::ref 是传递引用,std::cref 是传递什么呢?🤔
回答:在线程函数参数传递过程中,std::cref 是用于传递 const 引用的函数模板。它接受一个对象并返回一个 std::reference_wrapper<const T> 对象,其中 T 是对象类型。
使用 std::cref 的主要目的是在不拷贝对象的情况下传递对象给线程函数,并且保证线程函数在使用对象时不会对其进行修改。这对于并发编程很有用,因为它可以避免在多线程环境中出现数据竞争的情况。
示例代码如下:
#include <iostream>
#include <functional>
#include <thread>
void threadFunction(const int& x) {
std::cout << "Value of x in thread: " << x << std::endl;
}
int main() {
int value = 42;
// 使用 std::cref 传递引用给线程函数
std::thread t(threadFunction, std::cref(value));
t.join();
return 0;
}
在上面的示例中,我们使用 std::cref 来将变量 value 以引用的方式传递给线程函数 threadFunction。这样,线程函数可以读取 value 的值,但不能修改它。如果没有使用 std::cref,而是直接传递 value,则线程函数将获得 value 的拷贝,并且可能在主线程修改 value 时产生竞争条件。通过使用 std::cref,我们确保线程函数只能读取 value 而不会修改它。
问题5:我可以这样理解吗?std::ref 传递引用,而 std::cref 传递常引用,常引用参数只能读取而不能修改?🤔
回答:Yes, 您的理解是正确的!
std::ref 用于传递普通引用,允许线程函数对传递的对象进行读取和修改。
std::cref 用于传递常引用,允许线程函数读取传递的对象,但不允许对其进行修改。常引用确保线程函数在并发执行时不会改变传递的对象,从而帮助避免数据竞争和并发问题。
这两个函数模板在多线程编程中非常有用,它们可以帮助确保线程函数正确地使用传递的参数,从而提高多线程程序的安全性和可靠性。
问题6:有个疑问,在前面提到的线程数据同步的过程中,我们是创建了 promise 和 future 来实现的,但是我发现 future 这个变量其实可以不用创建,我可以先创建一个 promise,然后在线程启动函数中以引用的方式传递 promise,在拿结果的时候完全可以通过 promise 拿,即通过 std::string result = promise.get_future().get(),那么创建 future 的目的是什么呢?似乎作用不大呀🤔
回答:您的观察是正确的!在使用 promise 和 future 实现线程数据同步时,确实可以不必显式地创建 future 对象,因为可以通过 promise 对象的 get_future() 成员函数来隐式地创建与之相关联的 future 对象。
具体来说,通过调用 promise 对象的 get_future() 函数,可以获得一个与该 promise 相关联的 future 对象,该 future 对象用于在后续代码中获取异步任务的结果。因此,创建 promise 对象时,实际上也就同时创建了与之关联的 future 对象。
所以,如果您只需要在线程启动函数中传递异步任务的结果,并在其他地方获取该结果,而不需要显式地操作 future 对象,那么确实可以省略创建 future 对象的步骤,直接通过 promise 对象来获取结果。
总的来说,创建 future 对象的主要目的是让线程启动函数和其他代码之间能够更灵活地进行数据交换和通信,以满足不同的线程同步需求。但在某些情况下,如果只需要简单地传递异步任务的结果,使用 promise 就足够了,并且可以省略创建 future 对象的步骤。
总结
本次课程我们学习了线程的一些基本知识,包括创建线程、启动线程、线程启动函数参数传递以及类成员函数作为线程启动函数。线程启动后可以 join 也可以 detach,线程未启动则不能 join,join 会等待子线程执行完成,而 detach 会分离主线程和子线程,这时子线程成为了野线程,主线程没法 join 等待野线程,该线程会交给系统管理。在线程启动函数中如果要传递引用则通过 std::ref 实现,如果传递常引用则通过 std::cref 实现,如果我们想要让类的成员函数作为线程启动函数,我们传入的是该成员函数的指针,而并非该成员函数本身。最后我们探讨了利用 promise 和 future 实现线程间的数据传递和同步。关于多线程的更多知识可以参考下苏老师的 C++教程