深入理解std::shared_ptr:原理、用法及其執行緒安全性

非法关键字發表於2024-11-04

在 C++ 中,智慧指標是現代記憶體管理的重要工具,尤其是在複雜的多執行緒環境中,能顯著減少記憶體洩漏和懸空指標等問題。std::shared_ptr 是 C++11 引入的一種共享智慧指標,透過引用計數機制管理物件的生命週期。本文將詳細介紹 std::shared_ptr 的基本用法、迴圈引用問題、執行緒安全性及其侷限性。


1. 什麼是 std::shared_ptr

std::shared_ptr 是 C++ 標準庫中的一種智慧指標,允許多個指標共享管理同一個物件的生命週期。它透過引用計數(reference count)來記錄有多少個指標指向同一個物件,當引用計數為零時,std::shared_ptr 會自動釋放物件,避免手動管理記憶體帶來的風險。

#include <iostream>
#include <memory>

void example() {
    std::shared_ptr<int> p1 = std::make_shared<int>(10); // p1 引用計數為 1
    std::shared_ptr<int> p2 = p1; // p1 和 p2 都指向同一個 int,引用計數為 2

    std::cout << *p1 << std::endl; // 輸出 10

    p2.reset(); // p2 被重置,引用計數減少為 1
} // 作用域結束,p1 被銷燬,引用計數為 0,物件被釋放

在上面的例子中,std::shared_ptr 可以安全地管理記憶體的分配和釋放,保證了在作用域結束時物件被自動釋放。


2. std::shared_ptr 的優勢

使用 std::shared_ptr 帶來了以下主要優勢:

  • 自動釋放:當最後一個 std::shared_ptr 離開作用域時,引用計數變為零,自動呼叫物件的解構函式,防止記憶體洩漏。
  • 物件共享:多個 std::shared_ptr 可以指向同一物件,簡化了資源共享的實現。
  • 異常安全std::shared_ptr 的引用計數會自動管理,不會因為函式異常退出而洩漏記憶體。

這些優勢使 std::shared_ptr 特別適合用於物件共享和複雜的生命週期管理。


3. 迴圈引用問題

儘管 std::shared_ptr 帶來了諸多便利,但它的引用計數機制也可能帶來迴圈引用問題。迴圈引用發生在兩個或多個物件相互引用對方的 std::shared_ptr,導致引用計數永遠無法歸零,進而造成記憶體洩漏。

示例:迴圈引用問題

#include <iostream>
#include <memory>

class B; // 前向宣告

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B destroyed\n"; }
};

void example() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();

    a->b_ptr = b;
    b->a_ptr = a;

    // 離開作用域時,A 和 B 的解構函式不會被呼叫,造成記憶體洩漏
}

在上述程式碼中,AB 互相持有 std::shared_ptr,因此即使 example 結束,ab 的引用計數也不會歸零,導致解構函式未被呼叫。為了解決迴圈引用問題,C++ 提供了 std::weak_ptr


4. 使用 std::weak_ptr 打破迴圈引用

std::weak_ptr 是一種弱引用,它不會影響 std::shared_ptr 的引用計數,因此可以避免迴圈引用問題。std::weak_ptr 的主要作用是打破迴圈引用,同時提供一種安全的方式來訪問 std::shared_ptr 所管理的物件。

示例:使用 std::weak_ptr 解決迴圈引用

#include <iostream>
#include <memory>

class B;

class A {
public:
    std::weak_ptr<B> b_ptr; // 使用 weak_ptr 避免迴圈引用
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 使用 weak_ptr 避免迴圈引用
    ~B() { std::cout << "B destroyed\n"; }

    void useA() {
        if (auto shared_a = a_ptr.lock()) { // 使用 lock() 獲取 shared_ptr
            std::cout << "Using A\n";
        } else {
            std::cout << "A 已被釋放,無法使用\n";
        }
    }
};

void example() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();

    a->b_ptr = b;
    b->a_ptr = a;

    b->useA(); // 輸出 "Using A"
}

在這個例子中,AB 使用 std::weak_ptr 互相引用,這樣就不會增加引用計數,從而避免了迴圈引用的問題。std::weak_ptrlock() 方法會嘗試返回一個有效的 std::shared_ptr,如果物件已經被釋放,則返回空的 std::shared_ptr,這樣可以安全地檢查物件是否有效。


5. std::shared_ptr 的執行緒安全性

std::shared_ptr 提供了基本的執行緒安全性,保證了引用計數的執行緒安全更新。這意味著多個執行緒可以安全地同時持有和複製同一個 std::shared_ptr,引用計數的遞增和遞減操作會被正確地同步。

執行緒安全性帶來的好處

  • 引用計數執行緒安全:在多執行緒環境中,std::shared_ptr 的引用計數更新是原子操作,無需額外的加鎖操作。
  • 自動釋放的執行緒安全性:在最後一個 std::shared_ptr 離開作用域時,std::shared_ptr 會自動釋放物件,而這一過程在多執行緒中是安全的。

示例:多執行緒使用 std::shared_ptr

#include <iostream>
#include <memory>
#include <thread>

void thread_func(std::shared_ptr<int> ptr) {
    std::cout << "Thread: " << *ptr << std::endl;
}

void example() {
    auto shared_int = std::make_shared<int>(42);

    std::thread t1(thread_func, shared_int);
    std::thread t2(thread_func, shared_int);

    t1.join();
    t2.join();
} // 作用域結束,shared_int 被自動釋放

在這個例子中,shared_int 在兩個執行緒之間共享,std::shared_ptr 自動管理引用計數,並確保在多執行緒環境下引用計數的更新是安全的,避免了計數錯誤和資源釋放問題。

注意事項:雖然 std::shared_ptr 確保了引用計數的執行緒安全,但對物件本身的訪問並非執行緒安全。如果多個執行緒要修改 std::shared_ptr 指向的物件,仍然需要額外的同步措施(如使用 std::mutex)來保證執行緒安全。


6. 多執行緒修改 std::shared_ptr 指向的物件

如果多個執行緒需要同時訪問並修改 std::shared_ptr 指向的物件,使用 std::mutex 可以保證執行緒安全。這裡提供一個示例展示如何使用 std::mutex 來保護對共享物件的訪問和修改。

示例:多執行緒修改 std::shared_ptr 指向的物件

在這個例子中,我們建立一個共享的計數器物件,多個執行緒將同時訪問並修改該計數器。在沒有 std::mutex 保護的情況下,計數器的值可能會因資料競爭而出現錯誤。透過在訪問和修改計數器的程式碼塊中新增互斥鎖,我們可以確保每個執行緒按順序訪問該資源,避免資料競爭。

#include <iostream>
#include <memory>
#include <thread>
#include <mutex>
#include <vector>

class Counter {
public:
    int value;

    Counter() : value(0) {}
    void increment() {
        ++value;
    }
    int getValue() const {
        return value;
    }
};

void thread_func(std::shared_ptr<Counter> counter, std::mutex& mtx) {
    for (int i = 0; i < 100; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // 加鎖保護對 counter 的訪問
        counter->increment();
    }
}

int main() {
    auto counter = std::make_shared<Counter>();
    std::mutex mtx;

    std::vector<std::thread> threads;

    // 啟動10個執行緒,每個執行緒對 counter 執行 100 次 increment 操作
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(thread_func, counter, std::ref(mtx));
    }

    // 等待所有執行緒完成
    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Final counter value: " << counter->getValue() << std::endl; // 期望輸出 1000

    return 0;
}

在這個例子中,Counter 類的物件由 std::shared_ptr 管理,並在多個執行緒中共享,在 thread_func 函式中,每次呼叫 counter->increment() 前,都用 std::lock_guard<std::mutex> 鎖定 mtx,保證每次訪問 increment() 是原子操作,std::lock_guardRAII 風格的鎖管理器,它會在程式碼塊結束時自動釋放鎖。啟動 10 個執行緒,每個執行緒對共享計數器執行 100 次增量操作。透過 std::mutex,我們保證了計數器的修改是執行緒安全的。

程式輸出:Final counter value: 1000

在沒有互斥鎖的情況下,counter->increment() 在多個執行緒中可能會發生競爭,導致最終計數值低於預期的 1000。使用 std::mutex 來保護對共享資源的訪問,保證了執行緒安全,確保最終計數器值為 1000。

相關文章