关于函数返回、互斥锁解锁与对象拷贝的执行顺序及线程安全的技术问询
先聊聊你给出的代码场景和输出结果,咱们一步步拆解你的问题:
首先回顾核心代码片段(简化版):
// 全局互斥锁与Widget对象 std::mutex mtx; Widget shared_widget(0); // 自定义锁守卫,构造加锁、析构解锁 struct my_lock_guard { my_lock_guard(std::mutex& m) : mtx_(m) { m.lock(); } ~my_lock_guard() { mtx_.unlock(); } std::mutex& mtx_; }; // 返回Widget引用的函数 Widget& consumer() { my_lock_guard lk(mtx); return shared_widget; } int main() { Widget w = consumer(); // 触发拷贝构造 return 0; }
对应的输出顺序是:
Widget::Widget(int) my_lock_guard::my_lock_guard(std::mutex&) my_lock_guard::~my_lock_guard() Widget::Widget(const Widget&) Widget::~Widget() Widget::~Widget()
Q1:输出显示拷贝构造在互斥锁解锁后调用,这期间shared_widget会不会被其他线程修改?
完全有可能被修改,这是明确的线程安全风险点。
原因很直接:consumer()里的my_lock_guard lk是局部变量,当函数执行到return shared_widget;时,只是返回了指向shared_widget的引用,函数的栈帧还没开始销毁。等函数完全返回后,栈帧才会开始清理,局部变量lk的析构函数被调用——也就是这时候才执行mtx.unlock()。
而Widget w = consumer();里的拷贝构造,是在consumer()完全返回、锁已经解锁之后才执行的。这中间的时间窗口里,如果有其他线程(比如你提到的producer())成功获取到mtx锁,就可以随意修改shared_widget。这会导致最终拷贝到w的是被修改后的值,更糟的情况是如果shared_widget的修改不是原子操作,拷贝过程可能拿到半修改的无效数据,引发未定义行为。
Q2:能否把Widget w = consumer();拆分为两步?这期间会不会创建临时对象?
你的拆分是完全准确的,编译器处理这条语句的逻辑确实可以拆成:
// step1:调用consumer(),得到指向shared_widget的引用 Widget& temp_ref_ = consumer(); // step2:用引用初始化w,触发拷贝构造 Widget w = temp_ref_;
这里不会创建临时Widget对象,因为consumer()返回的是Widget&类型的引用,直接用这个引用来初始化w,本质就是把shared_widget的内容直接拷贝到w的内存空间里。
而你关心的“step1之后、step2之前,其他线程能不能修改shared_widget?”——答案和Q1一致:当然可以。因为consumer()返回时,内部的my_lock_guard已经析构解锁,shared_widget不再被互斥锁保护,任何能拿到锁的线程都能修改它。
补充:怎么解决这个线程安全问题?
如果要保证拷贝shared_widget的过程是线程安全的,你需要让拷贝操作在锁的保护范围内执行。最简单的方式是让consumer()返回Widget值而非引用:
Widget consumer() { my_lock_guard lk(mtx); return shared_widget; // 拷贝构造在锁的保护下执行 }
这样,shared_widget的拷贝构造会在lk析构(解锁)之前完成,整个拷贝过程都处于互斥锁的保护下,其他线程无法在这期间修改shared_widget,从根本上避免了线程安全风险。
内容来源于stack exchange

