單例模式中的隱藏陷阱:你真的了解單例嗎?

引言

? 單例模式是日常開發(fā)中較常用的一種設(shè)計模式,它能夠確保一個類只有一個實例,并提供一個全局訪問入口。

? 在之前的開發(fā)過程中,觀察到進(jìn)程退出時偶現(xiàn) crash 現(xiàn)象。由于該問題僅發(fā)生在生命周期末端且未影響核心功能,并未深入排查原因。近期看到技術(shù)公眾號分析單例模式潛在問題的,意識到此前的crash大致就是因此導(dǎo)致的。

場景列舉

問題
? 先看代碼,看看執(zhí)行會發(fā)生什么?

main 函數(shù)

  • 首先獲取 SingletonB 的實例。
  • 然后獲取 SingletonA 的實例。
  • 最后輸出退出信息。
int main() {
    SingletonA::getInstance();
    SingletonB::getInstance();
    std::cout << "exe exit!" << std::endl;
    return 0;
}

單例類 SingletonB

  • SingletonB 類使用靜態(tài)局部變量 instance 來確保只有一個實例。
  • 構(gòu)造函數(shù)中,mpBuf 分配了內(nèi)存,并將字符串 "hello" 復(fù)制到該內(nèi)存中。
  • 析構(gòu)函數(shù)中,釋放了 mpBuf 所占用的內(nèi)存,并置為nullptr。
// 單例B
class SingletonB {
public:
    ~SingletonB() {
        std::cout << "SingletonB destroyed." << std::endl;
        delete []mpBuf;     // 回收資源
        mpBuf = nullptr;
    }

    static SingletonB* getInstance() {
        static SingletonB instance;
        return &instance;
    }
    void doSomething() {
        printf("mpBuf[0] = %c\n", mpBuf[0]);
    }

private:
    SingletonB() {
        std::cout << "SingletonB created." << std::endl;
        mpBuf = new (std::nothrow) char[10];   // 創(chuàng)建資源
        memcpy(mpBuf, "hello", 5);
        mpBuf[5] = '\0';
    }

private:
    char *mpBuf;
};

單例類 SingletonA

  • SingletonA 類同樣使用靜態(tài)局部變量 instance 來確保只有一個實例。
  • 構(gòu)造函數(shù)中,簡單輸出創(chuàng)建信息。
  • 析構(gòu)函數(shù)中,調(diào)用 SingletonBdoSomething 接口。
// 單例A
class SingletonA {
private:
    SingletonA() {
        std::cout << "SingletonA created." << std::endl;
    }
    ~SingletonA() {
        std::cout << "SingletonA destroy begin. call A" << std::endl;
        SingletonB::getInstance()->doSomething();
        std::cout << "SingletonA destroyed end." << std::endl;
    }


public:
    static SingletonA* getInstance() {
        static SingletonA instance;
        return &instance;
    }
};

輸出

程序在退出時,發(fā)生crash了。

$ ./a.out 
SingletonA created.
SingletonB created.
exe exit!
SingletonB destroyed.
SingletonA destroy begin. call A
Segmentation fault

代碼分析

?上述的實現(xiàn),很明顯可以看出因為調(diào)用了已析構(gòu)的對象,導(dǎo)致異常crash。

?但是在實際項目開發(fā)過程中,單例模式的調(diào)用分散在各個文件中,由于初始化順序難以控制,很容易出現(xiàn)調(diào)用已析構(gòu)對象的情況,進(jìn)而導(dǎo)致程序崩潰。這類問題一旦發(fā)生,往往難以迅速定位和解決。

? 看看AI怎么評價這段代碼:

Me: 魔鏡啊魔鏡,上述代碼有什么問題?

魔鏡:來了,老弟!這代碼毛病可不小吶~
     那 `SingletonA` 析構(gòu)的時候,還跑去調(diào)用 `SingletonB` 的方法。
     程序結(jié)束的時候,靜態(tài)對象是后構(gòu)造的先析構(gòu),這 SingletonB 不妥妥先析構(gòu)了,mpBuf也回收了。
     SingletonA 再去調(diào)用 SingletonB::doSomething,指定下機(jī)啊!
     哪個損粗寫的這代碼吶!回爐重造吧,趕緊的啊~
  
Me: 鄉(xiāng)村愛情看多了吧你,口音聽的上頭。
    正經(jīng)點,那怎么規(guī)避這類問題?

魔鏡:閑來無事,網(wǎng)上爬了幾集,真好看~
    有很多種方式可以避免這種問題,我就說兩種,好好聽,好好學(xué)!
  
    ① 避免單例對象析構(gòu),如下寫法:
    SingletonA* SingletonA::getInstance() {
        static SingletonA *instance = new (std::nothorw) SingletonA();
        return instance;
    }
  
    ② 增加全局變量,檢測對象是否析構(gòu)。每次獲取實例時,特別是析構(gòu)中獲取單例時,先判空。
    static std::atomic<bool> gObjAlive(true);
    
    SingletonA::~SingletonA() {
        gObjAlive = false;
    }
  
    SingletonA* SingletonA::getInstance() {
        if (!gObjAlive) {
            return nullptr;
        }
  
        static SingletonA instance;
        return &instance;
    }

? 通過與AI的友好溝通,規(guī)避的方案有很多。上述的兩種方案,各有利弊:

  • 第一種方案
    優(yōu)點:寫法簡潔。
    缺點:由于不會調(diào)用析構(gòu)函數(shù),除內(nèi)存會隨進(jìn)程退出釋放外,析構(gòu)函數(shù)中涉及的硬件資源釋放等操作無法執(zhí)行,可能引發(fā)潛在問題。
  • 第二種方案
    優(yōu)點:安全性較高,可自動釋放所持資源。
    缺點:用時不夠便捷,每次調(diào)用單例對象(尤其是在析構(gòu)階段)都需要進(jìn)行判空操作。

總結(jié)

  • 結(jié)合全文,crash的問題根源在于單例析構(gòu)函數(shù)中調(diào)用其他單例對象的接口。
    進(jìn)程退出階段,全局單例對象的析構(gòu)順序未明確,可能導(dǎo)致調(diào)用了已釋放的單例對象接口,引發(fā)crash。
  • 盡管可以通過控制單例調(diào)用的順序來規(guī)避此類問題。但這種方式并非最優(yōu)解,而且大型項目中難以實施。另外,程序健壯性不應(yīng)依賴這種非確定性的語言特性,而應(yīng)通過設(shè)計優(yōu)化從根本上避免此類問題。
  • 最后,代碼運(yùn)行的行為和結(jié)果應(yīng)是可預(yù)見的,不應(yīng)依賴特殊操作或非確定性行為。若有此類依賴,應(yīng)是程序設(shè)計缺陷,該加以優(yōu)化。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容