MST

星途 面试题库

面试题:C++ std::shared_ptr引用计数的底层实现及线程安全问题

请深入分析std::shared_ptr引用计数的底层实现机制,包括引用计数的数据结构和更新策略。同时,讨论在多线程环境下,std::shared_ptr的引用计数可能会遇到哪些线程安全问题,以及如何解决这些问题(可以结合C++标准库中的线程同步工具进行阐述)。
34.9万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

1. std::shared_ptr引用计数的底层实现机制

引用计数的数据结构

std::shared_ptr 通常使用一个控制块(control block)来管理引用计数。这个控制块是一个动态分配的对象,包含以下主要成员:

  • 引用计数:记录当前有多少个 std::shared_ptr 指向同一个对象。它一般是一个 std::size_t 类型的变量。
  • 弱引用计数:用于 std::weak_ptr,记录当前有多少个 std::weak_ptr 指向同一个对象。同样是 std::size_t 类型。
  • 指向被管理对象的指针:指向实际被 std::shared_ptr 管理的对象。

例如,一个简化的控制块类可能如下:

class ControlBlock {
public:
    std::size_t refCount;
    std::size_t weakCount;
    void* object;

    ControlBlock(void* obj) : refCount(1), weakCount(0), object(obj) {}
};

更新策略

  • 初始化:当创建一个 std::shared_ptr 指向一个新对象时,会分配一个新的控制块,并将引用计数初始化为 1。
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
  • 拷贝构造:当通过拷贝构造函数创建一个新的 std::shared_ptr 时,它会共享同一个控制块,并将引用计数加 1。
std::shared_ptr<int> ptr2 = ptr1; 
  • 赋值:当进行赋值操作时,旧的 std::shared_ptr(如果存在)的引用计数减 1,新的 std::shared_ptr 的引用计数加 1。如果旧的 std::shared_ptr 的引用计数变为 0,会释放其管理的对象和控制块。
std::shared_ptr<int> ptr3;
ptr3 = ptr2; 
  • 析构:当 std::shared_ptr 析构时,引用计数减 1。如果引用计数变为 0,会释放被管理的对象以及控制块。
{
    std::shared_ptr<int> ptr4 = std::make_shared<int>(10);
} // 这里 ptr4 析构,引用计数减1,若变为0则释放对象和控制块

2. 多线程环境下的线程安全问题

  • 竞争条件:在多线程环境下,多个线程同时对引用计数进行操作(如增加或减少)可能会导致竞争条件。例如,一个线程读取引用计数的值,另一个线程同时修改了该值,可能导致引用计数出现不一致的情况,进而可能导致对象被提前释放或未被释放。
  • 双重释放:由于竞争条件,可能会出现两个线程同时认为引用计数变为 0,从而导致对象被释放两次,这会导致程序崩溃。

3. 解决线程安全问题的方法

使用互斥锁(std::mutex)

可以在控制块中使用 std::mutex 来保护对引用计数的操作。每次对引用计数进行增加、减少或读取操作时,都需要先锁定互斥锁。

class ControlBlock {
public:
    std::size_t refCount;
    std::size_t weakCount;
    void* object;
    std::mutex mtx;

    ControlBlock(void* obj) : refCount(1), weakCount(0), object(obj) {}
};

在更新引用计数的函数中:

void incrementRefCount(ControlBlock* cb) {
    std::lock_guard<std::mutex> lock(cb->mtx);
    cb->refCount++;
}

void decrementRefCount(ControlBlock* cb) {
    std::lock_guard<std::mutex> lock(cb->mtx);
    cb->refCount--;
    if (cb->refCount == 0) {
        // 释放对象和控制块
    }
}

使用原子操作(std::atomic)

C++ 标准库中的 std::atomic 类型提供了原子操作,这些操作是线程安全的。可以将引用计数定义为 std::atomic<std::size_t> 类型。

class ControlBlock {
public:
    std::atomic<std::size_t> refCount;
    std::atomic<std::size_t> weakCount;
    void* object;

    ControlBlock(void* obj) : refCount(1), weakCount(0), object(obj) {}
};

这样,对 refCountweakCount 的操作就不需要额外的锁来保证线程安全,因为原子操作本身就是线程安全的。

void incrementRefCount(ControlBlock* cb) {
    cb->refCount++;
}

void decrementRefCount(ControlBlock* cb) {
    if (--cb->refCount == 0) {
        // 释放对象和控制块
    }
}

在实际的 std::shared_ptr 实现中,通常会综合使用原子操作和互斥锁来优化性能并确保线程安全。例如,对引用计数的简单增减操作可以使用原子操作,而对于涉及复杂逻辑(如释放对象和控制块)的操作,可以使用互斥锁来保证一致性。