介绍一下ref

问题根源:线程参数传递的底层机制

1. 线程创建的底层过程

当调用std::thread构造函数时:

复制代码
thread t1(Print, 1000000, x, mtx);  // 错误方式
thread t2(Print, 1000000, std::ref(x), std::ref(mtx));  // 正确方式

底层发生了什么?

  • 线程库需要将参数打包成一个结构体

  • 这个结构体会被传递给系统级线程API(如pthread_create)

  • 系统API只能接受简单的void*指针

  • 因此参数会被拷贝到新线程的栈空间中

2. 查看thread库源码(文档中示例)

复制代码
template<class _Fn, class... _Args>
void _Start(_Fn&& _Fx, _Args&&... _Ax) {
    // 关键:参数包被打包成tuple,会发生拷贝!
    using _Tuple = tuple<decay_t<_Fn>, decay_t<_Args>...>;
    auto _Decay_copied = _STD make_unique<_Tuple>(
        _STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
    
    // 这个tuple对象被传递给新线程
    _Thr._Hnd = reinterpret_cast<void*>(_CSTD _beginthreadex(
        nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));
}

具体问题分析

3. 不用ref的问题演示

复制代码
#include <iostream>
#include <thread>
using namespace std;

void modify_value(int& x) {
    cout << "线程内修改前: " << x << endl;
    x = 100;  // 修改的是副本,不是原对象!
    cout << "线程内修改后: " << x << endl;
}

int main() {
    int value = 10;
    
    cout << "主线程调用前: " << value << endl;
    
    // 错误方式:直接传递引用
    thread t(modify_value, value);  // value被拷贝,不是引用!
    t.join();
    
    cout << "主线程调用后: " << value << endl;  // 还是10,没被修改!
    
    return 0;
}

输出结果:

复制代码
主线程调用前: 10
线程内修改前: 10  
线程内修改后: 100
主线程调用后: 10  // 值没有被修改!

4. 使用ref的正确方式

复制代码
#include <iostream>
#include <thread>
#include <functional>  // 需要包含ref的头文件
using namespace std;

void modify_value(int& x) {
    cout << "线程内修改前: " << x << endl;
    x = 100;
    cout << "线程内修改后: " << x << endl;
}

int main() {
    int value = 10;
    
    cout << "主线程调用前: " << value << endl;
    
    // 正确方式:使用ref包装引用
    thread t(modify_value, std::ref(value));  // 传递的是引用包装器
    t.join();
    
    cout << "主线程调用后: " << value << endl;  // 值被修改了!
    
    return 0;
}

输出结果:

复制代码
主线程调用前: 10
线程内修改前: 10
线程内修改后: 100
主线程调用后: 100  // 值被成功修改!

std::ref的魔法原理

5. std::ref的工作原理

std::ref()实际上创建了一个reference_wrapper对象:

复制代码
// 简化的reference_wrapper实现
template<typename T>
class reference_wrapper {
private:
    T* ptr;  // 存储指向原对象的指针
    
public:
    reference_wrapper(T& ref) : ptr(&ref) {}
    
    // 关键:隐式转换回引用
    operator T&() const { return *ptr; }
    
    T& get() const { return *ptr; }
};

6. 参数传递的详细过程

复制代码
// 原始调用
thread t(modify_value, std::ref(x));

// 编译器看到的实际过程:
reference_wrapper<int> temp = std::ref(x);  // 创建包装器
thread t(modify_value, temp);              // 传递包装器(按值拷贝)

// 在线程内部:
// 1. 包装器被拷贝到新线程
// 2. 调用函数时,包装器隐式转换为int&
// 3. modify_value接收到的是原对象的引用

7. 对比实验:理解拷贝过程

复制代码
#include <iostream>
#include <thread>
#include <functional>
using namespace std;

class TestObject {
public:
    int value;
    
    TestObject(int v) : value(v) {
        cout << "构造函数: " << value << endl;
    }
    
    TestObject(const TestObject& other) : value(other.value) {
        cout << "拷贝构造函数: " << value << endl;
    }
    
    ~TestObject() {
        cout << "析构函数: " << value << endl;
    }
};

void process_object(TestObject obj) {
    cout << "处理对象: " << obj.value << endl;
}

void process_reference(TestObject& obj) {
    cout << "处理引用: " << obj.value << endl;
}

int main() {
    TestObject obj(100);
    
    cout << "=== 直接传递对象(会发生拷贝)===" << endl;
    thread t1(process_object, obj);
    t1.join();
    
    cout << "\n=== 使用ref传递引用(避免拷贝)===" << endl;
    thread t2(process_reference, std::ref(obj));
    t2.join();
    
    return 0;
}

特殊情况与替代方案

8. 使用lambda捕获(推荐替代方案)

复制代码
#include <iostream>
#include <thread>
using namespace std;

int main() {
    int x = 10;
    mutex mtx;
    
    // 方法1:使用lambda捕获引用(更简洁)
    auto task1 = [&x, &mtx](int n) {
        mtx.lock();
        for (int i = 0; i < n; i++) ++x;
        mtx.unlock();
    };
    
    thread t1(task1, 1000000);
    thread t2(task1, 1000000);
    
    t1.join();
    t2.join();
    cout << "lambda方式结果: " << x << endl;
    
    // 方法2:传统的函数+ref方式
    auto traditional_task = [](int n, int& rx, mutex& rmtx) {
        rmtx.lock();
        for (int i = 0; i < n; i++) ++rx;
        rmtx.unlock();
    };
    
    x = 10;  // 重置
    thread t3(traditional_task, 1000000, std::ref(x), std::ref(mtx));
    thread t4(traditional_task, 1000000, std::ref(x), std::ref(mtx));
    
    t3.join();
    t4.join();
    cout << "ref方式结果: " << x << endl;
    
    return 0;
}

总结

为什么必须用std::ref()

  1. 线程参数必须可拷贝:系统线程API要求参数能打包传递

  2. 引用不是对象:引用本身不能单独存在,必须绑定到对象

  3. std::ref创建可拷贝的引用包装器:它存储原对象的指针,可以安全拷贝

  4. 隐式转换机制:在需要时自动转换回真正的引用

最佳实践建议:

  • 简单情况:优先使用lambda捕获,更直观安全

  • 复杂情况:使用std::ref()显式传递引用

  • 避免错误:永远不要直接传递引用给线程构造函数

理解了这一机制,你就能避免很多多线程编程中的隐蔽bug!

相关推荐
!!!!8132 小时前
蓝桥备赛Day1
数据结构·算法
王老师青少年编程2 小时前
2024年信奥赛C++提高组csp-s初赛真题及答案解析(完善程序第2题)
c++·题解·真题·初赛·信奥赛·csp-s·提高组
夏鹏今天学习了吗2 小时前
【LeetCode热题100(99/100)】柱状图中最大的矩形
算法·leetcode·职场和发展
nbsaas-boot2 小时前
软件开发最核心的理念:接口化与组件化
开发语言
Trouvaille ~2 小时前
【Linux】进程间关系与守护进程详解:从进程组到作业控制到守护进程实现
linux·c++·操作系统·守护进程·作业·会话·进程组
lsx2024062 小时前
Java 对象概述
开发语言
啊阿狸不会拉杆2 小时前
《机器学习导论》第 9 章-决策树
人工智能·python·算法·决策树·机器学习·数据挖掘·剪枝
Mr_Xuhhh2 小时前
C++11实现线程池
开发语言·c++·算法
若水不如远方2 小时前
分布式一致性(三):共识的黎明——Quorum 机制与 Basic Paxos
分布式·后端·算法