面试题答案
一键面试异常安全性面临的挑战
- 数据竞争:多个线程同时调用
sharedGetMyClass()
时,可能会导致复杂初始化操作的竞态条件,例如部分初始化完成但未完全构造好对象就被其他线程使用,进而引发未定义行为。 - 资源泄漏:在初始化过程中,如果抛出异常,可能会导致已分配的资源(如内存、文件句柄等)无法正确释放,特别是在多线程环境下,这种情况更难排查和处理。
解决方案
- 同步机制:使用
std::mutex
来保护共享资源和初始化操作,确保同一时间只有一个线程能够进行初始化。 - 异常安全设计原则:采用 RAII(Resource Acquisition Is Initialization)机制来管理资源,确保资源在对象生命周期结束时自动释放,避免资源泄漏。
代码示例(C++)
#include <memory>
#include <mutex>
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor" << std::endl;
// 模拟复杂初始化操作,可能抛出异常
if (rand() % 2) {
throw std::runtime_error("Initialization failed");
}
}
~MyClass() {
std::cout << "MyClass destructor" << std::endl;
}
};
std::mutex myMutex;
std::unique_ptr<MyClass> sharedMyClass;
MyClass& sharedGetMyClass() {
static std::once_flag onceFlag;
std::call_once(onceFlag, []() {
std::lock_guard<std::mutex> lock(myMutex);
try {
sharedMyClass.reset(new MyClass());
} catch (...) {
// 处理异常,确保不会泄漏资源
std::cerr << "Exception caught during MyClass initialization" << std::endl;
throw;
}
});
return *sharedMyClass;
}
关键实现点解释
std::once_flag
和std::call_once
:std::once_flag
用于标记某个函数是否已经执行过一次,std::call_once
确保其关联的函数只被调用一次,无论有多少线程同时尝试调用。这避免了重复初始化的问题。std::lock_guard
:在std::call_once
内部的初始化函数中,使用std::lock_guard
来锁定std::mutex
。std::lock_guard
采用 RAII 机制,在构造时自动锁定互斥锁,在析构时自动解锁,确保互斥锁的正确管理。- 异常处理:在初始化
MyClass
对象时,如果抛出异常,catch
块会捕获异常,输出错误信息,并重新抛出异常,确保函数调用者能够处理异常,同时保证资源不会泄漏。因为std::unique_ptr
会在MyClass
对象构造失败时自动释放已分配的内存。
通过以上同步机制和异常安全设计原则,可以保证在多线程环境下,sharedGetMyClass()
函数返回的引用始终指向一个处于有效状态的 MyClass
对象,并且不会因为异常导致数据竞争或资源泄漏。