MST

星途 面试题库

面试题:C++内存模型与并发编程中的数据竞争

深入讲解C++内存模型,说明其对并发编程的影响。阐述数据竞争的概念、产生原因及其危害。给出一段多线程C++代码,其中包含潜在的数据竞争问题,要求分析并修改代码以避免数据竞争,同时说明采用的同步机制(如mutex、lock_guard、unique_lock等)的原理和选择依据。
45.8万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

C++内存模型

  1. 内存模型基础
    • C++内存模型定义了程序中内存访问的规则,它决定了在多线程环境下不同线程对内存中数据的读写操作如何相互影响。
    • 内存模型规定了线程之间的内存可见性。例如,一个线程对变量的修改何时能被其他线程看到。在没有适当同步的情况下,不同线程对同一变量的操作可能出现不可预测的结果。
  2. 对并发编程的影响
    • 在并发编程中,C++内存模型影响着多线程程序的正确性和性能。如果不遵循内存模型规则,可能导致数据竞争,进而引发未定义行为。例如,在多线程中读写共享变量而没有同步机制,不同线程可能看到变量的不同值,程序行为将变得不可预测。

数据竞争

  1. 概念 数据竞争是指在多线程程序中,当两个或多个线程同时访问共享数据,并且至少有一个访问是写操作时,如果没有适当的同步机制来协调这些访问,就会发生数据竞争。
  2. 产生原因
    • 多线程对共享资源的无序访问是数据竞争产生的主要原因。例如,多个线程同时对一个全局变量进行读写操作,没有任何同步手段来保证操作的顺序和可见性。
    • 编译器优化也可能加剧数据竞争问题。编译器可能会对代码进行重排序优化,在单线程环境下这种优化是正确的,但在多线程环境中如果没有正确同步,可能导致错误的结果。
  3. 危害
    • 数据竞争会导致程序出现未定义行为,这意味着程序可能产生任意结果,包括崩溃、错误输出等。例如,在一个银行转账的多线程程序中,如果存在数据竞争,可能导致转账金额错误,账户余额不一致等严重问题。

包含数据竞争的多线程C++代码及分析修改

#include <iostream>
#include <thread>
int shared_variable = 0;
void increment() {
    for (int i = 0; i < 10000; ++i) {
        shared_variable++;
    }
}
int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Final value: " << shared_variable << std::endl;
    return 0;
}
  1. 分析
    • 在这段代码中,shared_variable是共享变量,increment函数在两个线程中同时执行,对shared_variable进行读 - 修改 - 写操作。由于没有同步机制,这会导致数据竞争。例如,当一个线程读取shared_variable的值,还未进行写回操作时,另一个线程也读取了相同的值,然后两个线程都基于这个旧值进行修改并写回,最终结果会比预期的20000小。
  2. 修改代码
#include <iostream>
#include <thread>
#include <mutex>
int shared_variable = 0;
std::mutex mtx;
void increment() {
    std::lock_guard<std::mutex> lock(mtx);
    for (int i = 0; i < 10000; ++i) {
        shared_variable++;
    }
}
int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Final value: " << shared_variable << std::endl;
    return 0;
}
  1. 同步机制原理及选择依据
    • mutex(互斥锁)mutex是一种基本的同步原语,它通过锁定和解锁操作来保证同一时间只有一个线程可以访问受保护的共享资源。当一个线程调用mutexlock方法时,如果锁没有被其他线程持有,该线程获得锁并可以访问共享资源;如果锁已被持有,线程将阻塞等待,直到锁被释放。
    • lock_guardlock_guard是一个RAII(Resource Acquisition Is Initialization)类型的对象,它在构造时自动锁定关联的mutex,在析构时自动解锁。这确保了即使在函数由于异常等原因提前退出时,锁也能被正确释放,避免死锁。这里选择lock_guard是因为它简单易用,适用于需要在一个作用域内锁定互斥锁的场景,能自动管理锁的生命周期,减少手动管理锁带来的错误风险。
    • unique_lockunique_locklock_guard更灵活,它可以延迟锁定、在作用域内手动锁定和解锁等。例如,如果需要在不同条件下锁定或解锁互斥锁,unique_lock会更合适。但在本场景中,简单的作用域内锁定需求,lock_guard已经满足且代码更简洁,所以选择lock_guard