C++ 线程函数传参与返回值:`std::ref` 和 `std::move` 的精妙运用

好的,咱们这就开始!

各位观众,各位老铁,大家好!欢迎来到今天的 C++ 线程函数传参和返回值专题讲座。今天咱们聊聊一个让很多 C++ 程序员挠头的问题:如何在线程函数中安全、高效地传递参数,以及如何优雅地获取返回值。特别是 std::refstd::move 这两个小家伙,它们在多线程的世界里可是大有作为。

开场白:线程函数那些事儿

在 C++ 中,多线程编程是绕不开的话题。创建一个线程很简单,就像这样:

#include <iostream>
#include <thread>

void hello_world() {
    std::cout << "Hello from thread!" << std::endl;
}

int main() {
    std::thread t(hello_world);
    t.join(); // 等待线程结束
    return 0;
}

这段代码创建了一个线程,执行 hello_world 函数,然后主线程等待子线程结束。但是,如果我们需要给 hello_world 函数传递参数呢?问题就来了。

传参:值传递、引用传递、还是移动语义?

线程函数的参数传递方式,决定了数据在线程之间的共享和生命周期管理。最常见的方式有三种:值传递、引用传递和移动语义。

  • 值传递 (Pass by Value): 这是最简单的方式,将参数的值复制一份传递给线程函数。
#include <iostream>
#include <thread>

void print_value(int value) {
    std::cout << "Thread received value: " << value << std::endl;
}

int main() {
    int x = 10;
    std::thread t(print_value, x); // 值传递
    x = 20; // 修改主线程的 x 不影响子线程
    t.join();
    std::cout << "Main thread x: " << x << std::endl;
    return 0;
}

在上面的例子中,x 的值被复制一份传递给 print_value 函数。主线程修改 x 的值,不会影响子线程中的 value

  • 引用传递 (Pass by Reference): 使用引用传递,线程函数直接操作主线程中的变量。这需要特别小心,因为多个线程可能同时访问和修改同一个变量,导致数据竞争。
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx; // 互斥锁

void increment(int& counter) {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // 加锁
        ++counter;
    }
}

int main() {
    int counter = 0;
    std::thread t1(increment, std::ref(counter)); // 引用传递
    std::thread t2(increment, std::ref(counter)); // 引用传递

    t1.join();
    t2.join();

    std::cout << "Counter value: " << counter << std::endl;
    return 0;
}

注意,这里使用了 std::ref。如果没有 std::ref,编译器会尝试将 counter 的值复制一份传递给线程函数,这会导致编译错误,因为 increment 函数的参数类型是 int& (引用)。std::ref 的作用就是将 counter 包装成一个引用对象,告诉 std::thread 构造函数,我们要传递的是引用。同时,为了避免数据竞争,使用了互斥锁 std::mutex

  • 移动语义 (Move Semantics): 对于一些大型对象,复制的开销很大。这时,可以使用移动语义,将对象的所有权转移给线程函数,而不是复制。
#include <iostream>
#include <thread>
#include <vector>

void process_data(std::vector<int> data) {
    std::cout << "Thread processing data of size: " << data.size() << std::endl;
}

int main() {
    std::vector<int> data(1000000); // 创建一个大型 vector
    std::thread t(process_data, std::move(data)); // 移动语义
    //data.push_back(1);  // 避免此处使用data,因为其资源已经被move

    t.join();
    //std::cout << "Main thread data size: " << data.size() << std::endl; // 此时 data 的大小可能为 0
    return 0;
}

std::move(data)data 转换为右值引用,告诉 std::thread 构造函数,我们要将 data 的所有权转移给 process_data 函数。移动后,data 的状态变为 valid but unspecified。不要再使用移动后的 data,除非重新给它赋值。

std::refstd::move 的精妙之处

现在,我们来仔细看看 std::refstd::move 这两个小家伙。

  • std::ref: 它是一个函数模板,用于创建一个引用包装器。它的作用就是将一个变量包装成一个引用对象,以便传递给那些需要引用类型参数的函数,比如 std::thread 的构造函数。

    • 为什么要用 std::ref

    因为 std::thread 构造函数在接收参数时,会进行一次拷贝。如果你直接传递一个引用,实际上会被拷贝成一个值。但是线程函数期望接收的是一个引用,而不是一个值,所以我们需要使用 std::ref 将变量包装成一个引用对象,告诉 std::thread 构造函数,我们要传递的是引用。

    • std::ref 的使用场景:

      1. 线程函数需要修改主线程中的变量: 正如上面的 increment 例子。
      2. 避免拷贝大型对象: 虽然移动语义更常用,但在某些情况下,直接使用引用可能更简单。
  • std::move: 它是一个函数,用于将一个左值转换为右值引用。它的作用不是移动任何东西,而是告诉编译器,我们可以将这个对象当作右值来处理,从而触发移动语义。

    • 为什么要用 std::move

    因为 C++ 中区分左值和右值。左值是可以出现在赋值语句左边的值,比如变量名。右值是只能出现在赋值语句右边的值,比如临时对象。移动语义只能应用于右值。因此,我们需要使用 std::move 将左值转换为右值,才能触发移动语义。

    • std::move 的使用场景:

      1. 转移大型对象的所有权: 正如上面的 process_data 例子。
      2. 提高性能: 避免不必要的拷贝。

表格总结 std::refstd::move

特性 std::ref std::move
作用 创建引用包装器,将变量包装成引用对象 将左值转换为右值引用,触发移动语义
传递方式 引用传递 移动语义
对象状态 不改变原对象的状态 原对象的状态变为 valid but unspecified
使用场景 线程函数需要修改主线程中的变量,避免拷贝大型对象 转移大型对象的所有权,提高性能

返回值:获取线程的计算结果

除了传递参数,我们还经常需要从线程函数中获取返回值。C++ 中获取线程返回值的方式有很多种,最常见的是使用 std::futurestd::promise

  • std::futurestd::promise: std::promise 用于在线程函数中设置返回值,std::future 用于在主线程中获取返回值。
#include <iostream>
#include <thread>
#include <future>

int calculate_sum(int a, int b) {
    std::cout << "Thread calculating sum..." << std::endl;
    return a + b;
}

int main() {
    std::packaged_task<int(int, int)> task(calculate_sum); // 使用 packaged_task
    std::future<int> future = task.get_future();
    std::thread t(std::move(task), 5, 3); //移动 packaged_task

    int sum = future.get(); // 获取返回值,会阻塞直到线程完成
    std::cout << "Sum: " << sum << std::endl;

    t.join();
    return 0;
}

或者

#include <iostream>
#include <thread>
#include <future>

int main() {
    std::promise<int> promise;
    std::future<int> future = promise.get_future();

    std::thread t([&](std::promise<int> p) {
        std::cout << "Thread calculating..." << std::endl;
        int result = 5 + 3;
        p.set_value(result); // 设置返回值
    }, std::move(promise)); // 移动 promise 对象

    int sum = future.get(); // 获取返回值,会阻塞直到线程完成
    std::cout << "Sum: " << sum << std::endl;

    t.join();
    return 0;
}

在这个例子中,std::promise 对象 promise 在主线程中创建,然后通过 std::move 移动到子线程中。子线程计算完成后,使用 promise.set_value(result) 设置返回值。主线程通过 future.get() 获取返回值。future.get() 会阻塞,直到子线程设置了返回值。

注意事项:

  • std::future 只能 get() 一次。多次调用会抛出异常。
  • 如果线程函数没有设置返回值,future.get() 会抛出异常。
  • std::promise 只能 set_value() 一次。多次调用会抛出异常。

更高级的用法:异步任务 (Asynchronous Tasks)

C++ 标准库还提供了 std::async 函数,用于创建异步任务。std::async 可以自动管理线程的创建和销毁,简化了多线程编程。

#include <iostream>
#include <future>

int calculate_product(int a, int b) {
    std::cout << "Thread calculating product..." << std::endl;
    return a * b;
}

int main() {
    std::future<int> future = std::async(std::launch::async, calculate_product, 5, 3); // 创建异步任务
    //std::future<int> future = std::async(std::launch::deferred, calculate_product, 5, 3); // 延迟执行

    int product = future.get(); // 获取返回值,会阻塞直到线程完成
    std::cout << "Product: " << product << std::endl;

    return 0;
}

std::async 的第一个参数是一个启动策略,可以是 std::launch::async (表示立即创建一个新线程执行任务) 或 std::launch::deferred (表示延迟到调用 future.get() 时才执行任务,在当前线程中执行)。如果没有指定启动策略,由系统决定。

总结:多线程传参和返回值

多线程编程需要小心谨慎,尤其是在共享数据和传递参数时。std::refstd::move 是两个非常有用的工具,可以帮助我们安全、高效地传递参数。std::futurestd::promise 以及 std::async 则提供了方便的机制来获取线程的返回值。记住,多线程编程的黄金法则:

  • 数据竞争要避免: 使用互斥锁、原子变量等同步机制。
  • 生命周期要管理: 确保线程访问的数据在线程运行期间有效。
  • 异常安全要考虑: 即使线程函数抛出异常,也要确保资源被正确释放。

掌握了这些技巧,你就可以在 C++ 的多线程世界里自由翱翔了!

最后的温馨提示:

多线程编程是一个复杂的话题,需要不断学习和实践。希望今天的讲座能对你有所帮助。记住,代码要多写,bug 要多改,才能成为真正的 C++ 大师!

谢谢大家!下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注