C++單利模式常見問題

C++語言是一門具有面向對象的高性能語言,其執行效率不小於C語言的80%。所以在現在的深度學習中被廣泛使用,深度學習在跑的實例需要大量的計算資源,算法在實現上又要實現模型的抽象。綜合考慮,C++是其載體的不二之選,模型驗證可以使用更加方便的python等高級語言,方便修改和編寫簡單。實際在工程中使用的還是以C++作爲模型的載體來實現算法。

C++是一門結合高性能和具有面向對象抽象的語言,同比其他語言具有這麼多的優點,爲什麼其他語言沒有達到?以我的觀點,正是因爲需要運行高性能使得他的寫法更偏向C,且沒有自動內存管理等優點,寫出的代碼健壯性在初級或者中級開發人員手中不容易保證,所以在實際業務方面使用的會很少,再比如java,雖然性能比C++低,但是從初級或者中級開發人員手中寫出的代碼無論從風格統一還是健壯性上來看,都遠遠高於C++的版本。也正是因爲C++的具有的這些特點,所以其語法和各個知識點都需要開發人員無比細緻的知曉,比如單例模式的應用。在我們實際用到單例模式的時候,往往覺得單例很簡單。無非是將構造函數私有化,然後提供一個全局的靜態類方法來進行訪問該類的資源。但是在我們實際工程中,對此還是需要有一定的原理了解。

在我公司實際項目中使用單例是一種非常普遍的事。但是不是每個人都能瞭解到其中的細節,有可能會對整個工程都帶來災難性的影響。

在下面列舉兩個不同單例的寫法,看上去類似,但是實際上有着很不同的作用。

代碼A:


class classForSingleton3
{
public:
    static classForSingleton3 *getClassForSingleton3()
    {

        static classForSingleton3 *m = 0;
        if (m == 0)
        {
            m = new classForSingleton3();
        }
        return m;
    }

private:
    classForSingleton3();
};

代碼B:


class classForSingleton4
{
public:
    static classForSingleton4 *getClassForSingleton4()
    {
        static classForSingleton4 m;
        return &m;
    }

private:
    classForSingleton4();
};

以上兩種都是單例的寫法,在實現上是達成了一致的效果,這個類無法被外部初始化,只能通過get方法獲取。

但是在實際運用上是有差異的,代碼A的單例並不是線程安全的寫法,代碼B纔是線程安全的寫法。

原因有這麼幾點:

1 一個類實例的構造函數是符合原子特性,只有執行完了,纔有進入的可能。這一點,我在很多C++編程相關的書上都沒有找到介紹。例如有一個類實例叫做a,a在使用的時候,會是多線程環境中使用,初始化的的時候,必定是在某一個線程中做的,而不是兩個線程都做一遍。

2 代碼A是先生成一個匿名實例,然後將匿名實例賦值給了函數內靜態變量m,構造函數實際沒有與m發生關聯,在實際運行過程中,該構造函數可能會被同時調用兩次。這一點我已經在下方提供的代碼中證明過。

3 代碼B構造函數是與函數內靜態變量m發生直接關係,依據類的構造函數原子性原理,該實例只會生成一份。且執行一次,是目前最安全的解決方式。

4 十分推薦單例獲取函數寫在cpp文件中。

代碼結構:

class1.h

文件中帶有4個單例類的實現方式。

#ifndef __CLASS_1_H__
#define __CLASS_1_H__
#include "stdio.h"
#include <unistd.h>
#define LOG(id, fmt, ...) printf("thread %d at[%s:%d] " fmt "\n", id, __FUNCTION__, __LINE__, ##__VA_ARGS__);

class classForSingleton1
{
public:
    static classForSingleton1 *getClassForSingleton1();

private:
    classForSingleton1();
};

class classForSingleton2
{
public:
    static classForSingleton2 *getClassForSingleton2();

private:
    classForSingleton2();
};

class classForSingleton3
{
public:
#if 0
    static classForSingleton3 *getClassForSingleton3()
    {

        if (mclassForSingleton3 == 0)
        {
            mclassForSingleton3 = new classForSingleton3();
        }
        return mclassForSingleton3;
    }
#else

    static classForSingleton3 *getClassForSingleton3()
    {
        static classForSingleton3 *m = 0;
        if (m == 0)
        {
            m = new classForSingleton3();
        }
        return m;
    }

#endif
private:
    static classForSingleton3 *mclassForSingleton3;
    classForSingleton3();
};
class classForSingleton4
{
public:
    static classForSingleton4 *getClassForSingleton4()
    {
        static classForSingleton4 m;
        return &m;
    }

private:
    classForSingleton4();
};

#endif

classForSingleton1 是我推薦的方式,使用初始化函數的靜態局部變量保存類實例。該類在測試的時候,按照預期得到唯一的實例。

classForSingleton2 是使用一個類指針保存當前實例,在多線程環境中,最終表現爲,不同的其他cpp調用的時候得到的實例不一致,而且生成了兩個實例的時候,最終也是不同的線程/cpp使用的實例不一致

classForSingleton3 與2一致,但是獲取單例的代碼寫在了h文件中,其單例類也被初始化了兩次,但是在最終使用的時候,有一個造成了內存泄漏,多個cpp使用的是同一個實例。

classForSingleton4 代碼與1一致,其表現也與1一致。

 

class1.cpp

文件中,寫入了四個單例類的構造函數。爲了模擬多線程競爭的問題,每個構造函數都sleep一秒,且在進入構造和退出構造的時候都打印相關字符串。

#include "class1.h"

#include "stdlib.h"
#include "stdio.h"

classForSingleton3 *classForSingleton3::mclassForSingleton3 = 0;

classForSingleton1 *classForSingleton1::getClassForSingleton1()
{
    static classForSingleton1 m;
    return &m;
}

classForSingleton2 *classForSingleton2::getClassForSingleton2()
{
    static classForSingleton2 *m = 0;
    if (m == 0)
    {
        m = new classForSingleton2();
    }
    return m;
}

classForSingleton1::classForSingleton1()
{
    printf("classForSingleton1 start at addr: 0x%X\n", this);
    usleep(1000 * 1000);
    printf("classForSingleton1 end\n");
}

classForSingleton2::classForSingleton2()
{
    printf("classForSingleton2 start at addr: 0x%X\n", this);
    usleep(1000 * 1000);
    printf("classForSingleton2 end\n");
}

classForSingleton3::classForSingleton3()
{
    printf("classForSingleton3 start at addr: 0x%X\n", this);
    mclassForSingleton3 = 0;
    usleep(1000 * 1000);
    printf("classForSingleton3 end\n");
}

classForSingleton4::classForSingleton4()
{
    printf("classForSingleton4 start at addr: 0x%X\n", this);
    usleep(1000 * 1000);
    printf("classForSingleton4 end\n");
}

function1.h

定義的單例引用函數getS1_1,getS2_1,getS3_1,getS4_1,其內部實現如下面


#ifndef __FUNCTION1_H__
#define __FUNCTION1_H__

void getS1_1(int id);
void getS1_2(int id);
void getS1_3(int id);
void getS1_4(int id);

#endif

function1.cpp

#include "function1.h"
#include "class1.h"

void getS1_1(int id)
{
    void *ptr = classForSingleton1::getClassForSingleton1();
    LOG(id, "get addr : 0x%X", (int *)ptr);
    usleep(100);
    ptr = classForSingleton1::getClassForSingleton1();
    LOG(id, "get addr : 0x%X", (int *)ptr);
}
void getS1_2(int id)
{
    void *ptr = classForSingleton2::getClassForSingleton2();
    LOG(id, "get addr : 0x%X", (int *)ptr);
    usleep(100);
    ptr = classForSingleton2::getClassForSingleton2();
    LOG(id, "get addr : 0x%X", (int *)ptr);
}
void getS1_3(int id)
{
    void *ptr = classForSingleton3::getClassForSingleton3();
    LOG(id, "get addr : 0x%X", (int *)ptr);
    usleep(100);
    ptr = classForSingleton3::getClassForSingleton3();
    LOG(id, "get addr : 0x%X", (int *)ptr);
}
void getS1_4(int id)
{
    void *ptr = classForSingleton4::getClassForSingleton4();
    LOG(id, "get addr : 0x%X", (int *)ptr);
    usleep(100);
    ptr = classForSingleton4::getClassForSingleton4();
    LOG(id, "get addr : 0x%X", (int *)ptr);
}

實現了四個get函數的訪問對應單例類。而且每個都訪問兩次,查看其中的變動

function2.h與function2.cpp與function1.h和function1.cpp一致,就不放其內容,可以自己從代碼上下下來,自己查閱。

 

test.cpp


#include "class1.h"
#include "function1.h"
#include "function2.h"
#include <thread>
#include <unistd.h>
using namespace std;
volatile int gIsOk = 0;
void firstThread()
{
    int id = 0;
    while (gIsOk != 1)
    {
    }
    LOG(id, "get s1 start");
    getS1_1(id);
    LOG(id, "get s1 end\n");
    gIsOk++;
    while (gIsOk != 3)
    {
    }
    LOG(id, "get s2 start");
    getS1_2(id);
    LOG(id, "get s2 end\n");
    gIsOk++;
    while (gIsOk != 5)
    {
    }
    LOG(id, "get s3 start");
    getS1_3(id);
    LOG(id, "get s3 end\n");
    gIsOk++;
    while (gIsOk != 7)
    {
    }
    LOG(id, "get s4 start");
    getS1_4(id);
    LOG(id, "get s4 end\n");
}
void secondThread()
{
    int id = 1;
    while (gIsOk != 1)
    {
    }
    LOG(id, "get s1 start");
    getS2_1(id);
    LOG(id, "get s1 end\n");
    gIsOk++;
    while (gIsOk != 3)
    {
    }
    LOG(id, "get s2 start");
    getS2_2(id);
    LOG(id, "get s2 end\n");
    gIsOk++;
    while (gIsOk != 5)
    {
    }
    LOG(id, "get s3 start");
    getS2_3(id);
    LOG(id, "get s3 end\n");
    gIsOk++;
    while (gIsOk != 7)
    {
    }
    LOG(id, "get s4 start");
    getS2_4(id);
    LOG(id, "get s4 end\n");
}

int main()
{
    thread t1(firstThread);
    t1.detach();
    thread t2(secondThread);
    t2.detach();
    gIsOk = 1;
    while (1)
    {
        usleep(100);
    }
    return 0;
}

在這個文件中,是測試程序的入口,從main函數中,我們創建了兩個線程,模擬多線程競爭環境。也創建了一個變量。在每個線程中,分別調用不同的function的內容,最終的日誌輸出如下:

thread 0 at[firstThread:15] get s1 start
classForSingleton1 start at addr: 0x60B1B0
thread 1 at[secondThread:46] get s1 start
classForSingleton1 end
thread 0 at[getS1_1:7] get addr : 0x60B1B0
thread 1 at[getS2_1:8] get addr : 0x60B1B0
thread 0 at[getS1_1:10] get addr : 0x60B1B0
thread 0 at[firstThread:17] get s1 end
thread 1 at[getS2_1:11] get addr : 0x60B1B0
thread 1 at[secondThread:48] get s1 end

thread 1 at[secondThread:53] get s2 start
classForSingleton2 start at addr: 0xA00008C0
thread 0 at[firstThread:22] get s2 start
classForSingleton2 start at addr: 0x980008C0
classForSingleton2 end
thread 0 at[getS1_2:15] get addr : 0x980008C0
classForSingleton2 end
thread 1 at[getS2_2:16] get addr : 0xA00008C0
thread 1 at[getS2_2:19] get addr : 0xA00008C0
thread 1 at[secondThread:55] get s2 end
thread 0 at[getS1_2:18] get addr : 0x980008C0
thread 0 at[firstThread:24] get s2 end

thread 0 at[firstThread:29] get s3 start
thread 1 at[secondThread:60] get s3 start
classForSingleton3 start at addr: 0xA00008E0
classForSingleton3 start at addr: 0x980008E0
classForSingleton3 end
thread 0 at[getS1_3:23] get addr : 0x980008E0
classForSingleton3 end
thread 1 at[getS2_3:24] get addr : 0xA00008E0
thread 1 at[getS2_3:27] get addr : 0xA00008E0
thread 1 at[secondThread:62] get s3 end
thread 0 at[getS1_3:26] get addr : 0xA00008E0
thread 0 at[firstThread:31] get s3 end

thread 0 at[firstThread:36] get s4 start
classForSingleton4 start at addr: 0x60B1D0
thread 1 at[secondThread:67] get s4 start
classForSingleton4 end
thread 0 at[getS1_4:31] get addr : 0x60B1D0
thread 1 at[getS2_4:33] get addr : 0x60B1D0
thread 0 at[getS1_4:34] get addr : 0x60B1D0
thread 0 at[firstThread:38] get s4 end
thread 1 at[getS2_4:36] get addr : 0x60B1D0
thread 1 at[secondThread:69] get s4 end

可以從輸出中看到的是,兩個線程基本都是同時進入的每一對單例的獲取函數。

在singleton1中,實例的構造函數被執行了1遍,兩個線程都獲取的是同一個實例。

在singleton2中,實例構造函數被執行了兩次,且在多次獲取實例的時候,最終形成的是每個function1/2擁有自己的實例。互不干擾,這是非常可怕的事情,比如在實際運行的時候,a線程負責加載某些資源然後全局共享。最終導致的問題就是莫名的崩潰了。

在singleton3中,構造函數也被執行了兩次,但是可以注意到,在最終的第二次獲取單例的時候,有一個實例被內存泄漏了,兩個線程最終也是使用的同一個實例。也是非常危險的。

在singleton4中,構造函數被執行了一次,兩個cpp中獲取的實例也是一致的,但是這種寫法,建議還是與singleton1保持一致,將單例獲取函數的實現寫在cpp當中是最爲保險的。

這其中實際上有許多的c++編譯的細節在其中體現。過於展開又會比較繁瑣,就不一一說明了。

 

總結:

單例模式的使用,比我們想象中也有許多要注意的細節部分。建議參考singleton1的寫法,最爲安全。

代碼地址:github代碼地址

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章