面试题答案
一键面试std::shared_ptr 在异常处理下的线程安全
- 引用计数的原子操作:
std::shared_ptr
通过原子引用计数机制来管理资源。在多线程环境下,引用计数的增减操作都是原子的。这意味着,无论有多少线程同时对std::shared_ptr
进行赋值、拷贝或析构操作,引用计数的修改都不会出现数据竞争。例如,当一个线程创建std::shared_ptr
指向某个资源时,引用计数原子地增加;当std::shared_ptr
离开作用域或被重新赋值时,引用计数原子地减少。这种原子性确保了在多线程环境下引用计数的一致性。 - 资源释放的线程安全:由于引用计数的原子操作,当引用计数降为 0 时,资源的释放操作也是线程安全的。多个线程同时观察到引用计数为 0 时,只有一个线程会执行资源释放的操作(例如调用
delete
),其他线程会因为原子操作的特性而不会重复释放资源。
资源释放过程中的竞争条件及解决方案
- 竞争条件:虽然引用计数本身是线程安全的,但在资源释放的具体操作(如调用自定义的删除器)中仍可能存在竞争条件。例如,如果自定义删除器需要访问共享资源(除了要释放的目标资源外),而这个共享资源没有适当的同步机制,就可能出现竞争。
- 解决方案:
- 使用互斥锁:在自定义删除器中,如果涉及到访问共享资源,可以使用互斥锁来保护共享资源的访问。例如:
std::mutex resourceMutex;
void customDeleter(MyResource* res) {
std::lock_guard<std::mutex> lock(resourceMutex);
// 进行资源释放操作
delete res;
}
std::shared_ptr<MyResource> ptr(new MyResource(), customDeleter);
- **无状态删除器**:尽量使用无状态的删除器,避免删除器内部出现共享资源的访问。例如,使用默认的 `delete` 操作作为删除器,这样就不存在因删除器内部共享资源访问导致的竞争条件。
多线程操作并可能抛出异常时的代码设计
- 异常安全性:为了确保异常安全性,在对
std::shared_ptr
进行操作时,应尽量使用异常安全的函数。例如,使用std::make_shared
来创建std::shared_ptr
,因为它是异常安全的。std::make_shared
一次性分配内存用于控制块(包含引用计数等信息)和对象本身,减少了内存分配失败导致资源泄漏的风险。 - 资源正确管理:
- RAII 原则:始终遵循 RAII(Resource Acquisition Is Initialization)原则,将资源的管理封装在
std::shared_ptr
中。例如,在函数内部创建std::shared_ptr
来管理局部资源,这样当函数因为异常退出时,std::shared_ptr
的析构函数会自动释放资源。 - 异常安全的函数设计:如果函数内部涉及多个对
std::shared_ptr
的操作,且这些操作可能抛出异常,应确保函数整体是异常安全的。一种方法是使用try - catch
块捕获可能抛出的异常,并在捕获异常后进行适当的清理操作。例如:
- RAII 原则:始终遵循 RAII(Resource Acquisition Is Initialization)原则,将资源的管理封装在
void threadSafeFunction() {
std::shared_ptr<MyResource> ptr;
try {
ptr = std::make_shared<MyResource>();
// 对 ptr 进行其他操作,这些操作可能抛出异常
ptr->doSomething();
} catch(...) {
// 处理异常,此时 ptr 会自动释放资源
std::cerr << "Exception caught" << std::endl;
}
}
通过以上方式,可以在多线程环境下确保 std::shared_ptr
的异常安全性和资源的正确管理。