1. 程式人生 > >Qt下實現支援多執行緒的單例模式

Qt下實現支援多執行緒的單例模式

1. 程式碼介紹

實現單例模式的程式碼很多。
本文的單例模式實現程式碼是本人一直在工程專案中使用的,現拿出和大家交流分享。
本文實現的單例模式,支援多執行緒,採用雙重校驗檢索的方式,整合析構類,杜絕記憶體洩漏,穩定性好。 使用C++/Qt的朋友們可以瞭解一下。
不再廢話,直接上程式碼。

2. 程式碼之路

標頭檔案makelog.h

#include <QMutex>
#include <QObject>
class Makelog: public QObject
{
  	Q_OBJECT
  public:
  	static Makelog* getInstance()
  	{
  		if (m_pInstance == NULL)
  		{
  			QMutexLocker mlocker(&m_Mutex);  //雙檢索,支援多執行緒
  			if (m_pInstance == NULL)
  			{
  				m_pInstance = new Makelog();
  			}
  		}
  		return m_pInstance;
  	}
  private:
  	Makelog(){}
  	Makelog(const Makelog&){}
  	Makelog& operator ==(const Makelog&){}

  	static Makelog* m_pInstance;   //類的指標
  	static QMutex m_Mutex;
  public:
  	class CGarbo  //專用來析構m_pInstance指標的類
  	{
  		public:
  		~CGarbo()
  		{
  			if (m_pInstance != NULL)
  			{
  				delete m_pInstance;
  				m_pInstance = NULL;
  			}
  		}
  	};
  	static CGarbo m_Garbo; 					
};

makelog.cpp檔案

Makelog* Makelog::m_pInstance = NULL;
Makelog::CGarbo m_Garbo;
QMutex Makelog::m_Mutex;

支援多執行緒,無記憶體洩漏的單例模式就實現了。
下面舉例說明具體的使用:
在標頭檔案中加入一個public函式、一個public變數。

public:
void readFile();
QString m_config;

在原始檔中加入函式具體實現,使得readFile()或m_config有實際的意義。
那麼,在一個工程內的其他類中,只需做兩個步驟,就可以使用這個readFile()函式和m_config變量了。
步驟1:包含標頭檔案

#include “makefile.h”

步驟2:通過單例類入口呼叫函式或變數

Makelog::getInstance()->readFile(); //使用函式
Makelog::getInstance()->m_config; //使用變數

3. 詳細分析

3.1 什麼是單例

單例是一種軟體設計模式,採用單例模式書寫的類可以確保在一個工程中只有一個物件例項。再通俗點,就是一個類寫好了之後,就不需要也無法再把這個類例項化了,因為寫這個類的時候已經確保了有且僅有一個已經例項化的物件。
這樣不是很蠢麼?花了這麼多功夫寫了一個類,你告訴我這個類沒法用來new出物件了?那我怎麼使用這個類?我寫個配合靜態變數的靜態函式,使用起來不是更方便?
當然不蠢,非但不蠢,而且單例模式是所有設計模式中使用最為頻繁的一個設計模式。沒法new出物件,因為單例模式已經幫你new了一個物件,而且讓你的工程中只有這個物件了;使用這個物件只需要包含標頭檔案,然後呼叫介面指標函式就可以了;靜態的全域性函式或變數程式碼實現起來方便,但是不具有類的封裝性和靈活性。

3.2 如何讓類無法例項化

首先要清楚類例項化無非就是三種方式:
1)採用建構函式例項化;
2)用拷貝函式實現例項化;
3)賦值操作實現例項化。
所以,只需要把這個類的建構函式、拷貝函式、賦值操作寫成私有的,就無法呼叫這些函式,自然就無法例項化了。
正如上文所示的幾個函式:

private:
Makelog(){} //建構函式
Makelog(const Makelog&){} //拷貝函式
Makelog& operator ==(const Makelog&){} //賦值操作符重寫

當然,如果有一些初始化的操作,也可以寫在私有建構函式的雙括號內。

3.3 如何呼叫這個唯一例項

廣泛採用的做法就是在寫一個public的函式作為介面,這個函式返回單例類唯一例項的指標。
最簡單的寫法如下:

Makelog* getInstance()
{
if (m_pInstance == NULL)
{
m_pInstance = new Makelog(); //呼叫private建構函式,把唯一例項的指標例項化
}
return m_pInstance;
}
Makelog* m_pInstance; //唯一例項的指標

這個寫法看著真不錯,可是這麼寫遇到了一個小小的悖論。
“我如何去呼叫執行這個getInstance()函式啊,對,我需要一個例項化物件才能去執行!那我去new個物件,等等,唯一的例項化物件是通過這個函式才能找到的啊!”
有方法解決麼,當然,類的靜態方法不需要例項化物件,用

類名::方法()

的形式就可以呼叫執行了,所以把getInstance()函式前加一個static就好了。
但是靜態方法只能使用靜態成員變數啊,那就把唯一例項的指標m_pInstance也變成static的吧。
Ok,這樣有沒有隱患啊?隱患?static型別的instance存在靜態儲存區,每次呼叫時,都指向的同一個物件。非但沒有隱患,簡直堪稱完美!
現在上面的程式碼就變成這樣:

public:
static Makelog* getInstance()
{
if (m_pInstance == NULL )
{
m_pInstance = new Makelog(); //用私有建構函式new出了這個類的唯一物件
}
return m_pInstance;
}
private:
static Makelog* m_pInstance; //唯一例項指標

這樣寫完全沒有問題,但是不支援多執行緒的呼叫。因為new Makelog()需要時間,所以當兩個執行緒同時判斷m_pInstance ==NULL,同時執行了m_pInstance = new Makelog()這句程式碼,問題就大了。

3.4 如何支援多執行緒

為了解決3.3節產生的bug,廣泛採用的方式是雙重校驗檢索的方法。
就是利用互斥鎖(用來保證鎖內程式碼最多隻有一個執行緒在同時執行)的方式,確保不會出現兩個執行緒同時new出這個單例類的唯一例項的情況發生。
具體程式碼如下:

static Makelog* getInstance()
{
if (m_pInstance == NULL)
{
QMutexLocker mlocker(&m_Mutex); //加鎖,鎖內程式碼只有一個執行緒執行
if (m_pInstance == NULL) //先執行的執行緒會進入內部new物件,後一個執行緒判斷m_pInstance就不是NULL了
{
m_pInstance = new Makelog();
}
}
return m_pInstance;
}

至此雙重校驗檢索解決多執行緒問題的單例問題就解決了。當然還可以用原子鎖的方法來解決,但是靈活性不強(也可能是我太外行,靈活不起來—。—),這裡就不介紹了。

3.5 如何解決記憶體洩漏

解決單例類的記憶體析構主要就是解決static Makelog* m_pInstance這個指標的析構問題(畢竟其他的可以不用指標的嘛)。我總結覺得寫一個專門用來析構的類是最方便有效和無腦的方法了,推薦給大家。
具體就是在單例類中寫一個類:

public:
class CGarbo //專用來析構m_pInstance指標的類
{
public:
~CGarbo() //這個類只有解構函式
{
if (m_pInstance != NULL)
{
delete m_pInstance;
m_pInstance = NULL;
}
}
};
static CGarbo m_Garbo; //宣告一個靜態的物件

然後在cpp檔案中宣告一下 Makelog::CGarbo m_Garbo就可以了。
這個類只有解構函式,解構函式的作用就是delete單例唯一物件的指標。
析構類宣告一個static物件,因為靜態物件系統會在關閉程式時自動析構,就可以執行到解構函式內部的程式碼了。

4. 結束語

單例模式是非常常用而基礎的一個設計模式,本文作者第一次寫部落格,有不詳或錯誤之處還請大家指正。對於還在使用C++/Qt的初學者,請不要因為害怕而不去深究和掌握單例模式這個好用實用的工具。