MST

星途 面试题库

面试题:C++ 引用和指针在多线程环境下内存分配差异及优化

在多线程编程环境中,C++ 的引用和指针在内存分配上会面临哪些不同的挑战,例如内存竞争、数据一致性等问题?请详细阐述并给出相应的优化策略和代码示例。
38.3万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

引用和指针在多线程内存分配上的挑战

  1. 内存竞争
    • 指针
      • 多个线程可能同时访问和修改同一个指针所指向的内存。例如,一个线程可能正在释放指针指向的内存,而另一个线程还在尝试通过该指针访问这块内存,这会导致悬空指针问题,进而引发未定义行为。
      • 假设有如下代码:
#include <iostream>
#include <thread>
#include <memory>

int* globalPtr = nullptr;

void thread1() {
    globalPtr = new int(42);
}

void thread2() {
    if (globalPtr != nullptr) {
        std::cout << "Value: " << *globalPtr << std::endl;
    }
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);
    t1.join();
    t2.join();
    delete globalPtr;
    return 0;
}

在这段代码中,如果 thread2 先执行 if (globalPtr != nullptr) 判断,然后 thread1 执行 globalPtr = new int(42);,接着 thread2 执行 std::cout << "Value: " << *globalPtr << std::endl;,理论上没问题。但如果 thread1 先执行完 globalPtr = new int(42);thread2 开始执行,在 thread2 读取 *globalPtr 时,thread1 突然执行 delete globalPtr;,就会导致 thread2 访问已释放的内存。

  • 引用
    • 引用一旦初始化,就不能再指向其他对象,所以不存在悬空引用(除了对象生命周期结束时)。但是,多个线程同时访问和修改引用所绑定对象的内存同样会导致内存竞争。例如:
#include <iostream>
#include <thread>

int globalVar = 0;

void thread3() {
    int& ref = globalVar;
    ref = 10;
}

void thread4() {
    int& ref = globalVar;
    std::cout << "Value: " << ref << std::endl;
}

int main() {
    std::thread t3(thread3);
    std::thread t4(thread4);
    t3.join();
    t4.join();
    return 0;
}

这里如果 thread3thread4 同时执行,thread4 可能输出未被 thread3 修改前的值,或者在 thread3 修改一半时读取到错误的值。 2. 数据一致性

  • 指针
    • 由于指针可以随意修改指向,不同线程对指针的修改可能导致数据不一致。例如,一个线程将指针指向新的内存地址,而其他线程还在使用旧地址的数据,没有及时更新对新数据的引用。
  • 引用
    • 虽然引用不能改变指向,但多个线程对引用绑定对象的并发修改也可能导致数据不一致。比如,一个线程对引用绑定的对象进行部分修改,另一个线程在此时读取该对象,可能得到不完整或错误的数据。

优化策略

  1. 使用互斥锁(Mutex)
    • 指针场景
#include <iostream>
#include <thread>
#include <memory>
#include <mutex>

std::mutex ptrMutex;
int* globalPtr = nullptr;

void thread1() {
    std::lock_guard<std::mutex> lock(ptrMutex);
    globalPtr = new int(42);
}

void thread2() {
    std::lock_guard<std::mutex> lock(ptrMutex);
    if (globalPtr != nullptr) {
        std::cout << "Value: " << *globalPtr << std::endl;
    }
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);
    t1.join();
    t2.join();
    std::lock_guard<std::mutex> lock(ptrMutex);
    delete globalPtr;
    return 0;
}
  • 引用场景
#include <iostream>
#include <thread>
#include <mutex>

std::mutex refMutex;
int globalVar = 0;

void thread3() {
    std::lock_guard<std::mutex> lock(refMutex);
    int& ref = globalVar;
    ref = 10;
}

void thread4() {
    std::lock_guard<std::mutex> lock(refMutex);
    int& ref = globalVar;
    std::cout << "Value: " << ref << std::endl;
}

int main() {
    std::thread t3(thread3);
    std::thread t4(thread4);
    t3.join();
    t4.join();
    return 0;
}
  1. 使用智能指针
    • std::unique_ptr:在多线程环境中,如果每个线程有自己独立的 std::unique_ptr 实例,就可以避免多个线程对同一指针的竞争。例如:
#include <iostream>
#include <thread>
#include <memory>

void thread5() {
    std::unique_ptr<int> localPtr = std::make_unique<int>(42);
    std::cout << "Value in thread5: " << *localPtr << std::endl;
}

void thread6() {
    std::unique_ptr<int> localPtr = std::make_unique<int>(100);
    std::cout << "Value in thread6: " << *localPtr << std::endl;
}

int main() {
    std::thread t5(thread5);
    std::thread t6(thread6);
    t5.join();
    t6.join();
    return 0;
}
  • std::shared_ptr:如果需要多个线程共享对象,std::shared_ptr 内部的引用计数是线程安全的。但要注意对共享对象的访问可能仍需同步。例如:
#include <iostream>
#include <thread>
#include <memory>
#include <mutex>

std::mutex sharedMutex;
std::shared_ptr<int> sharedPtr;

void thread7() {
    std::lock_guard<std::mutex> lock(sharedMutex);
    if (!sharedPtr) {
        sharedPtr = std::make_shared<int>(42);
    }
    std::cout << "Value in thread7: " << *sharedPtr << std::endl;
}

void thread8() {
    std::lock_guard<std::mutex> lock(sharedMutex);
    if (sharedPtr) {
        std::cout << "Value in thread8: " << *sharedPtr << std::endl;
    }
}

int main() {
    std::thread t7(thread7);
    std::thread t8(thread8);
    t7.join();
    t8.join();
    return 0;
}