7.2.tensorRT高级(2)-学习深度学习中涉及的线程知识

news2024/11/24 7:42:20

目录

    • 前言
    • 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.”。

执行效果如下:

在这里插入图片描述

图1-1 完整启动线程案例执行效果

我们来考虑下几个问题,首先就是 join 必须加吗?不加会怎样呢?

我们不加 join 的执行效果如下:

在这里插入图片描述

图1-2 不加join执行效果

可以看到程序在 return 0; 的时候直接崩溃了,因此我们得到第一个规则,即

t.join 如果不加,会在析构时提示异常,出现 core dumped,因此只要线程 t 启动了,就必须要 join,没有启动则不必要 join

如果我们的线程 t 没有启动,但是我们执行了 join,会发生什么呢?

我们不启动线程,但是执行 t.join,执行效果如下:

在这里插入图片描述

图1-3 不启动线程但加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 的,线程交给系统管理,程序退出后,所有线程才退出

执行效果如下:

在这里插入图片描述

图1-4 detach执行效果

我们可以看到 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;
}

执行效果如下:

在这里插入图片描述

图1-5 detach等待执行效果

了解完线程启动的相关知识后,我们来看下线程启动函数的参数是怎么传递的,示例代码如下所示:

#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;
}

运行效果如下:

在这里插入图片描述

图1-6 线程启动函数传值

可以看到传递的变量 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;
}

运行效果如下:

在这里插入图片描述

图1-7 线程启动函数传引用

可以看到 reference string 被正常打印出来了,因此你如果要在线程函数中传引用,一定要用 std::ref() 的方式去传递,传完之后就可以去访问和修改,还有一个细节,传进去的参数是存在生命周期的,如果在线程启动函数在执行的过程中,引用参数已经销毁的话,则程序会发生崩溃,这是关于线程启动函数传参的相关知识

接下来我们来看下类成员函数作为线程启动函数的使用,我们不难写出下面的示例代码:

class Infer{
public:
    
    Infer(){
        
        worker_thread_ = thread(infer_worker);
    }

private:

    void infer_worker(){
        
    }

private:
    thread worker_thread_;
};

运行效果如下:

在这里插入图片描述

图1-8 类成员函数作为线程启动函数报错

可以看到报错了,它提供说 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::promisestd::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::promisestd::future 这两个类模板。

  • std::promise:是一个模板类,它允许在一个线程中产生一个值,并在另一个线程中获得这个值。我们可以将一个 std::promise 关联到一个 std::future,从而允许线程间进行数据的传递。
  • std::future:也是一个模板类,它表示一个可能在某个时间点获得的值。通过关联到 std::promise,可以在一个线程中获取 std::promise 中设置的值。

使用 std::promisestd::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::promisestd::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++教程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/873679.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

7. 延迟队列

延迟队列 7.1. 延迟队列概念 延时队列,队列内部是有序的&#xff0c;最重要的特性就体现在它的延时属性上&#xff0c;延时队列中的元素是希望 在指定时间到了以后或之前取出和处理&#xff0c;简单来说&#xff0c;延时队列就是用来存放需要在指定时间被处理的 元素的队列。 7…

第二章-算法

第二章-算法 数据结构和算法的关系 算法是解决特定问题求解步骤的描述&#xff0c;在计算机中表现为指令的有限序列&#xff0c;并且每条指令表示一个或多个操作。 算法的特性 算法有五个基本特征&#xff1a;输入、输出、有穷性、确定性和可行性。 输入&#xff1a;算法具…

LVS集群和nginx负载均衡

目录 1、基于 CentOS 7 构建 LVS-DR 群集。 2、配置nginx负载均衡。 1、基于 CentOS 7 构建 LVS-DR 群集。 1.部署LVS负载调度器 1>安装配置工具 [rootnode6 ~]# yum install -y ipvsadm 2>配置LVS虚拟IP&#xff08;VIP地址&#xff09; [rootnode6 ~]# ifconfig ens…

Tesseract用OpenCV进行文本检测

我没有混日子&#xff0c;只是辛苦的时候没人看到罢了 一、什么是Tesseract Tesseract是一个开源的OCR&#xff08;Optical Character Recognition&#xff09;引擎&#xff0c;OCR是一种技术&#xff0c;它可以识别和解析图像中的文本内容&#xff0c;使计算机能够理解并处理…

Dalsa线阵相机说明(Linea Color GigESeries 2k and 4K)

文章目录 一. Dalsa相机软件整体架构二. 相机编号说明以及软件要求三. 相机硬件参数三. 相机基本参数四. 软件参数设置列表1. Sensor Control Category2. I/O Control Category3. Counter and Timer Control Category4. Advanced Processing Control Category(1) 平场校正介绍(…

蚂蚁聚合支付系统源码完美版+附安装教程

这是我上个月花重金购买的四fang系统源码&#xff0c;现在分享给大家。 源码内附安装教程&#xff0c;20多项功能及安全方面的更新文档&#xff0c;源码说明等&#xff0c;小白也能轻松搭建。 能够轻松应对高并发&#xff0c;等以前版本无法应对的并发问题&#xff0c;也不会被…

JIRA:项目管理的秘密武器

引言 在当今动态且快速变化的商业环境中&#xff0c;项目管理已经成为任何组织成功的关键因素。能够有效地管理项目&#xff0c;保证项目在设定的时间和预算内按照预期的质量完成&#xff0c;是每个项目经理的目标。为了实现这个目标&#xff0c;项目经理需要依赖强大的工具&a…

由于找不到vcruntime140_1.dll,无法继续执行代码该怎么修复呢?

我最近遇到了一个vcruntime140_1.dll的问题&#xff0c;导致我的某些应用程序无法正常运行。当我第一次遇到这个问题时&#xff0c;我对这个dll文件的作用一无所知。我开始搜索并了解了vcruntime140_1.dll是Microsoft Visual C Redistributable的一部分&#xff0c;并为使用Vis…

FreeRTOS(计数信号量)

资料来源于硬件家园&#xff1a;资料汇总 - FreeRTOS实时操作系统课程(多任务管理) 目录 一、计数信号量的定义与应用 1、计数信号量的定义 2、计数信号量的应用 二、计数信号量的运作机制 1、任务间计数信号量的实现 三、计数信号量常用的API函数 1、计数信号量典型流程…

泛型编程| 模板初阶——懒人福音!

目录 前言介绍 函数模板 函数模板格式 函数模板的原理 函数模板的实例化 隐式实例化 显示实例化 模板参数匹配规则 类模板 总结 前言介绍 之前c语言实现swap函数的时候&#xff0c;我们不仅要修改参数的类型&#xff0c;还要修改函数的名字 而在学完函数重载之后&am…

东南大学齿轮箱故障诊断(Python代码,MSCNN结合LSTM结合注意力机制模型,代码有注释)

运行代码要求&#xff1a; 代码运行环境要求&#xff1a;Keras版本>2.4.0&#xff0c;python版本>3.6.0 1.东南大学采集数据平台&#xff1a; 数据 该数据集包含2个子数据集&#xff0c;包括轴承数据和齿轮数据&#xff0c;这两个子数据集都是在传动系动力学模拟器&am…

Android JNI实现锅炉压力显示系统详解

前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下"通俗易懂&#xff0c;风趣幽默"&#xff0c;感觉非常有意思,忍不住分享一下给大家。 &#x1f449;点击跳转到教程 第一步创建GuoLu.c文件 // // Created by DELL on 2023/8/13. // #include <stdio.h…

Python 潮流周刊#15:如何分析 FastAPI 异步请求的性能?

你好&#xff0c;我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容&#xff0c;大部分为英文。标题取自其中一则分享&#xff0c;不代表全部内容都是该主题&#xff0c;特此声明。 本周刊精心筛选国内外的 250 信息源&#xff0c;为你挑选最值得分享的文章、教程、开源…

记录一下关于word存放代码出现的问题

如下图所示&#xff0c;从 Word 中复制代码并粘贴到其他地方&#xff0c;例如文本编辑器或代码编辑器中&#xff0c;有时会出现额外的连字符或破折号。这是因为 Word 使用了特定的字符编码和格式&#xff0c;而这些字符在代码中可能不被支持或解析为特殊字符。   可见有时从…

Java线程调度以及算法

线程调度概况 Java的线程调度程序是JVM的一部分&#xff0c;它决定应该运行哪个线程。无法保证线程调度程序将选择运行哪个可运行线程。 一次只能有一个线程在一个进程中运行。线程调度程序主要使用抢占式或时间切片调度来调度线程。 抢占式调度与时间分片的区别 在抢占式调…

边缘计算:下一代计算模式的突破

章节一&#xff1a;引言 随着物联网、人工智能和大数据等技术的不断发展&#xff0c;计算需求变得越来越复杂&#xff0c;传统的云计算模式已经难以满足快速增长的数据处理需求。在这样的背景下&#xff0c;边缘计算作为一种全新的计算模式崭露头角&#xff0c;为我们带来了更加…

Pytorch深度学习-----实现神经网络模型在GPU上进行训练的方法

系列文章目录 PyTorch深度学习——Anaconda和PyTorch安装 Pytorch深度学习-----数据模块Dataset类 Pytorch深度学习------TensorBoard的使用 Pytorch深度学习------Torchvision中Transforms的使用&#xff08;ToTensor&#xff0c;Normalize&#xff0c;Resize &#xff0c;Co…

css3新增选择器总结

目录 一、属性选择器 二、结构伪类选择器 三、伪元素选择器 四、UI状态伪类选择器 五、反选伪类选择器 六、target选择器 七、父亲选择器、后代选择器 八、相邻兄弟选择器、兄弟们选择器 一、属性选择器 &#xff08;除IE6外的大部分浏览器支持&#xff09; E&#…

数据库设计,理解第二范式和第三范式的区别

在学习数据库范式的时候&#xff0c;很多人搞不清第二范式和第三范式他到底是有啥区别。 想讲清楚第二范式与第三范式的区别&#xff0c;不得不聊到键和相关属性的概念 键和相关属性的概念 范式的定义会使用到主健和候选健&#xff0c;数据库中的健&#xff08;Key)由一个或…

STM32F103C8T6开发笔记1:有线陀螺仪二自由度机械臂

经过之前几天的快速学习&#xff0c;今日尝试组装一款基于MPU6050陀螺仪控制的二自由度机械臂&#xff0c;本文对其使用器材以及基本原理进行介绍~ 组装效果图&#xff1a; 主要元器件如下&#xff1a; 器件个数15 KG以上 舵机3适合舵机的金属夹爪118650电池电源12V1云台支架2…