Linux:線程安全的單例模式及STL、智能指針與線程安全

單例模式

特點:某些類,只具有一個對象(實例)稱爲單例,自行實例化並向整個系統提供這個實例,例如我們實現的線程池,緩存等。
常見的單例模式有懶漢模式和餓漢模式。
總結
單例模式的特點:
(1)單例類只能有一個實例
(2)單例類必須創建自己的唯一實例
(3)單例類必須給其他對象提供這一對象實例
單例模式的優點:
(1)單例模式只能創建一個對象,所以在資源方面可以做到節約資源
(2)單例模式不需要頻繁地銷燬和創建,所以在效率方面有所提高
(3)單例對象在整個系統裏面只有一份,可以做到避免共享資源的重複佔用
(4)單例模式的對象必須向整個系統提供,所以可以做到全局

  • 餓漢模式
    在程序初始化時加載一次資源,運行過程就不再重新加載了
    例如:
class Singleton
{
public:
	static Singleton& GetInstrance()
	{
		return m_ins;
	}
private:
	Singleton()
	{}
	static Singleton m_ins;//程序啓動時對象創建好,通過Singleton這個包裝類來使用T對象,一個進程中只有一個T對象的實例
	Singleton(const Singleton&) = delete;
	Singleton& operator=(Singleton const&) = delete;
};
Singleton Singleton::m_ins;//初始化
int main()
{
	Singleton& s = Singleton::GetInstrance();
	return 0;
}

分析:如果這個單例對象在多線程高併發環境下頻繁使用,性能要求較高,那麼顯然使用餓漢模式來避免資源競爭,提高響應速度更好,且加載進行時靜態創建單例對象,線程安全。缺點是無論是否使用,總要創建,浪費內存

  • 懶漢模式
    懶漢模式最核心的思想就是“延時加載”,從而優化服務器的啓動速度(例如寫時拷貝)
    如果單例對象構造十分耗時或者佔用很多資源,比如加載插件啊, 初始化網絡連接啊,讀取文件啊等等,而有可能該對象程序運行時不會用到,那麼也要在程序一開始就進行初始化,就會導致程序啓動時非常的緩慢。 所以這種情況使用懶漢模式(延遲加載)更好。
    優點是:什麼時候用什麼時候創建,節約內存,,缺點是在第一次調用訪問獲取實例對象的靜態接口時才真正創建,如果在多線程操作情況下有可能被創建出多個實例化對象,存在線程不安全問題
    它的實現方式有兩種:(1)靜態指針+用到時初始化;(2)局部靜態變量
    爲什麼叫他懶漢模式,就是不到調用GetInstrance函數,這個類的對象就是不存在的
    我們下面寫的是靜態指針寫法
    例如:
#include <mutex>
#include <thread>
//懶漢模式,第一次使用時創建,延遲加載
//不是線程安全的,不能保證只能創建一個對象,因此實現加鎖功能
//容易造成線程阻塞,利用雙判斷
//volatile作用是禁止編譯器對代碼發生指令重排
class Singleton
{
public:
	static volatile Singleton* GetInstrance()
	{
		if (nullptr == m_ins)//加一層檢測,防止線程阻塞
		{
			m_mutex.lock();//加鎖
			if (nullptr == m_ins)//若爲空,說明是第一次調用
				m_ins = new Singleton;
			m_mutex.unlock();//解鎖
		}
		return m_ins;
	}
	class GC
	{
	public:
		~GC()
		{
			if (m_ins)
			{
				delete m_ins;
				m_ins = nullptr;
			}
		}
	};//內嵌垃圾回收類
private:
	Singleton()
	{}

	Singleton(const Singleton&) = delete;
	static volatile Singleton* m_ins;//在使用時創建對象
	static mutex m_mutex;
	static GC m_gc;
};
volatile Singleton*  Singleton::m_ins = nullptr;
mutex Singleton::m_mutex;
Singleton::GC m_gc;

分析:懶漢模式是在第一次使用時創建對象,上述volatile的作用是防止過度優化及防止指令重排,線程每次獲取volatile變量的值都是最新的;並且要記得加鎖,因爲如果不加鎖,有可能在調用GetInstance時,如果兩個線程同時調用,可能會創建出兩份T對象的實例,因此要注意線程安全。使用雙重if判定,避免不必要的鎖競爭。

STL,智能指針和線程安全
  • STL中的容器
    STL中的容器不是線程安全的,因爲STL的設計初衷是將性能挖掘到極致,一旦涉及加鎖保證線程安全,會對性能產生巨大的影響,而且對於不同的容器,加鎖方式的不同,性能也可能不同,因此STL默認不是線程安全的,如果實在多線程環境下使用,往往需要調用者自行保證線程安全。
  • 智能指針
    對於unique_ptr,因爲只在當前代碼塊內生效,因此不涉及線程安全問題,對於shared_ptr,多個對象需要共用一個引用計數變量,所以會存在線程安全問題,但是標準庫實現的時候已經考慮到了此問題,基於原子操作保證shared_ptr能夠高效,原子的操作引用計數。
  • 其他常見的鎖
    悲觀鎖:在每次取數據時,總是擔心數據會被其他線程修改,所以會在取數據前先加鎖(讀鎖、寫鎖、執行鎖等),當其他線程想要訪問數據時,被阻塞掛起(例如互斥鎖)。
    樂觀鎖:每次取數據時,總是樂觀的以爲數據不會被其他數據修改,因此不上鎖,但是在更新數據前,會判斷其他數據在更新前是否對數據進行修改,主要採用2種方式:版本號機制和CAS操作。
    CAS操作:當需要更新數據時,判斷當前內存值和之前所取得的值是否相等,如果相等用新值更新,若不相等則失敗,失敗就是重試,一般是一個自旋的過程,即不斷重試。
    當在臨界資源待的時間比較短的時候,推薦使用自旋鎖(自旋狀態),當在臨界資源待的時間比較長時,推薦使用掛起等待鎖(之前我們所學習的基本都是掛起等待鎖),因爲如果在臨界資源待的時間比較短,使用掛起等待鎖要進行掛起和喚醒,花費的時間比較大,不推薦使用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章