首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

雙重檢查鎖定模式(DCLP)為什麼不可行?C++單例模式線程安全深度剖析

     編輯:關於C語言

單例模式與線程安全問題

單例是最常用的設計模式,但基本實現非線程安全:

Singleton* Singleton::instance() {
    if (pInstance == 0) {        // Line 1
        pInstance = new Singleton; // Line 2
    }
    return pInstance;
}

多線程下,線程 A 執行 Line 1 判斷為空後被掛起,線程 B 執行完創建對象返回,線程 A 恢復執行 Line 2 再次創建,導致兩個實例。

加鎖版本:性能代價大

Singleton* Singleton::instance() {
    Lock lock;
    if (pInstance == 0) pInstance = new Singleton;
    return pInstance;
}

每次調用都加鎖,開銷過大。實際只需首次創建時加鎖。

DCLP 嘗試:去掉不必要的鎖

Singleton* Singleton::instance() {
    if (pInstance == 0) {        // 1st test
        Lock lock;
        if (pInstance == 0) {    // 2nd test
            pInstance = new Singleton;
        }
    }
    return pInstance;
}

看似完美:首次檢查無鎖,只有為空時才加鎖再檢查一次。但存在致命缺陷。

核心問題:指令重排序

pInstance = new Singleton 實際分三步:

  1. 分配內存 operator new(sizeof(Singleton))
  2. 構造對象 new (pInstance) Singleton
  3. 賦值指針 pInstance = ...

編譯器為優化可能交換步驟 2 和 3(先賦值指針,再構造對象)。若線程 A 執行完 1、3 後被掛起,此時 pInstance != NULL 但對象未構造完成。線程 B 進入首次檢查發現非空,直接返回未構造完成的指針,解引用導致崩潰。

為什麼 C/C++ 無法保證順序?

  • C++ 標準未規定 new 表達式的評估順序
  • 編譯器優化、CPU 亂序執行、緩存一致性均可能導致可見性亂序
  • 只有當構造函數不拋出異常時,編譯器才可能合法重排;但語言層面無此保證

正確解決方案

C++11 及以上:利用靜態局部變量初始化線程安全特性

Singleton& Singleton::instance() {
    static Singleton instance; // C++11 保證線程安全初始化
    return instance;
}

C++11 前:使用 std::call_once / pthread_once / Windows InitOnceExecuteOnce

std::once_flag flag;
Singleton* Singleton::instance() {
    std::call_once(flag, []{ pInstance = new Singleton; });
    return pInstance;
}

雙重檢查鎖定修正版(需內存屏障/volatile/atomic)

std::atomic pInstance{nullptr};
std::mutex mtx;

Singleton* Singleton::instance() {
    Singleton* tmp = pInstance.load(std::memory_order_acquire);
    if (!tmp) {
        std::lock_guard lock(mtx);
        tmp = pInstance.load(std::memory_order_relaxed);
        if (!tmp) {
            tmp = new Singleton;
            pInstance.store(tmp, std::memory_order_release);
        }
    }
    return tmp;
}

總結

DCLP 在無內存模型保證的語言中本質上是破碎的。現代 C++ 應優先使用靜態局部變量或 std::call_once,簡單、安全、零開銷(首次調用外)。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved