天天看點

C/C++設計模式(一) 單例模式

概念

單例模式是一種對象建立型模式,使用單例模式,可以保證為一個類隻生成唯一的執行個體對象。也就是說,在整個程式空間中,該類隻存在一個執行個體對象。    

GoF對單例模式的定義是:保證一個類、隻有一個執行個體存在,同時提供能對該執行個體加以通路的全局通路方法。

C/C++設計模式(一) 單例模式

為什麼使用單例模式

在應用系統開發中,我們常常有以下需求:

  • 在多個線程之間,比如初始化一次socket資源;比如servlet環境,共享同一個資源或者操作同一個對象
  • 在整個程式空間使用全局變量,共享資源
  • 大規模系統中,為了性能的考慮,需要節省對象的建立時間等等。

因為Singleton模式可以保證為一個類隻生成唯一的執行個體對象,是以這些情況,Singleton模式就派上用場了。

實作單例步驟常用步驟 

  1. 構造函數私有化
  2. 提供一個全局的靜态方法(全局通路點)
  3. 在類中定義一個靜态指針,指向本類的變量的靜态變量指針

餓漢式單例和懶漢式單例

/*---懶漢式---*/
#include <iostream>
using namespace std;

class  Singelton
{
private:
    Singelton()
    {
        m_singer = NULL;
        m_count = 0;
        cout << "構造函數Singelton ... do" << endl;
    }

public:
    static Singelton *getInstance()
    {
        if (m_singer == NULL )  //懶漢式:1 每次擷取執行個體都要判斷 2 多線程會有問題
        {
            m_singer = new Singelton;
        }
        return m_singer;
    }

    static void printT()
    {
        cout << "m_count: " << m_count << endl;
    }

private:
    static Singelton *m_singer;
    static int m_count;
};

Singelton *Singelton::m_singer = NULL;  //懶漢式 并沒有建立單例對象
int Singelton::m_count = 0;

void main()
{
    cout << "懶漢式" << endl;
    Singelton *p1 = Singelton::getInstance(); //隻有在使用的時候,才去建立對象。
    Singelton *p2 = Singelton::getInstance();
    
    if (p1 != p2)
    {
        cout << "不是同一個對象" << endl;
    }
    else
    {
        cout << "是同一個對象" << endl;
    }

    p1->printT();
    p2->printT();

    system("pause");
    return ;
}
           
/*---俄漢式---*/
class  Singelton2
{
private:
    Singelton2()
    {
        m_singer = NULL;
        m_count = 0;
        cout << "構造函數Singelton ... do" << endl;
    }

public:
    static Singelton2 *getInstance()
    {
       if (m_singer == NULL )
       {
           m_singer = new Singelton2;
       }

        return m_singer;
    }

    static void Singelton2::FreeInstance()
    {
        if (m_singer != NULL)
        {
            delete m_singer;
            m_singer = NULL;
            m_count = 0;
        }
    }

    static void printT()
    {
        cout << "m_count: " << m_count << endl;
    }

private:
    static Singelton2 *m_singer;
    static int m_count;
};

Singelton2 *Singelton2::m_singer = new Singelton2; //不管你建立不建立執行個體,均把執行個體new出來
int Singelton2::m_count = 0;

void main()
{
    cout << "餓漢式" << endl;
    Singelton2 *p1 = Singelton2::getInstance(); //隻有在使用的時候,才去建立對象。
    Singelton2 *p2 = Singelton2::getInstance();

    if (p1 != p2)
    {
        cout << "不是同一個對象" << endl;
    }
    else
    {
        cout << "是同一個對象" << endl;
    }

    p1->printT();
    p2->printT();

    Singelton2::FreeInstance();
    Singelton2::FreeInstance();

    system("pause");
}
           

多線程下的懶漢式單例和餓漢式單例

1、"懶漢"模式雖然有優點,但是每次調用GetInstance()靜态方法時,必須判斷NULL == m_instance,使程式相對開銷增大。

2、多線程中會導緻多個執行個體的産生,進而導緻運作代碼不正确以及記憶體的洩露。

3、提供釋放資源的函數

這是因為C++中構造函數并不是線程安全的。

C++中的構造函數簡單來說分兩步:

第一步:記憶體配置設定

第二步:初始化成員變量

        由于多線程的關系,可能當我們在配置設定記憶體好了以後,還沒來得急初始化成員變量,就進行線程切換,另外一個線程拿到所有權後,由于記憶體已經配置設定了,但是變量初始化還沒進行,是以列印成員變量的相關值會發生不一緻現象。

多線程下的懶漢式問題抛出:

C/C++設計模式(一) 單例模式
#include "stdafx.h"
#include "windows.h"
#include "winbase.h"
#include <process.h>
#include "iostream"

using namespace std;
class Singelton
{
private:
	Singelton()
	{
		count ++;
		cout<<"Singelton構造函數begin\n"<<endl;
		Sleep(1000);
		cout<<"Singelton構造函數end\n"<<endl;
		
	}
private:
	//防止拷貝構造和指派操作
	Singelton(const Singelton &obj) { ;}
	Singelton& operator=(const Singelton &obj)	{ ;}
public:
	static Singelton *getSingelton()
	{
		//1"懶漢"模式雖然有優點,但是每次調用GetInstance()靜态方法時,必須判斷
		//	NULL == m_instance,使程式相對開銷增大。
		//2多線程中會導緻多個執行個體的産生,進而導緻運作代碼不正确以及記憶體的洩露。
		//3提供釋放資源的函數
		return single;
	}

	static Singelton *releaseSingelton()
	{
		if (single != NULL) //需要判斷
		{
			cout<<"釋放資源\n"<<endl;
			delete single;
			single = NULL;
		}
		return single;
	}
	void pirntS() //測試函數
	{
		printf("Singelton printS test count:%d \n", count);
	}

private:
	static Singelton *single;
	static int count;
};

//note 靜态變量類外初始化
Singelton *Singelton::single = new Singelton();
int Singelton::count = 0;

int _tmainTTT(int argc, _TCHAR* argv[])
{
	Singelton *s1 = Singelton::getSingelton();
	Singelton *s2 = Singelton::getSingelton();
	if (s1 == s2)
	{
		cout<<"ok....equal"<<endl;
	}
	else
	{
		cout<<"not.equal"<<endl;
	}
	s1->pirntS();
	Singelton::releaseSingelton();
	cout <<"hello...."<<endl;
	system("pause");
	return 0;
}

unsigned int threadfunc2(void *myIpAdd)
{
	int id = GetCurrentThreadId();
	printf("\n threadfunc%d \n", id);
	return 1;
}

void threadfunc(void *myIpAdd)
{
	int id = GetCurrentThreadId();
	printf("\n threadfunc%d \n", id);
	 Singelton::getSingelton()->pirntS();
	return ;
}

 int _tmain(int argc, _TCHAR* argv[])
{
	int i = 0; 
	DWORD dwThreadId[201], dwThrdParam = 1;
	HANDLE hThread[201]; 
	int threadnum = 3;

	for (i=0; i<threadnum; i++)
	{
		//hThread[i] = (HANDLE)_beginthreadex( NULL, 0, &threadfunc, NULL, 0,&dwThreadId[i] );
		hThread[i] = (HANDLE)_beginthread(&threadfunc, 0 , 0 );
		if (hThread[i] == NULL)
		{
			printf("begin thread %d error!!!\n", i);
			break;
		}		
	}
    //等待所有的子線程都運作完畢後,才執行 這個代碼
	for (i=0; i<threadnum; i++)
	{
		WaitForSingleObject( hThread[i], INFINITE );	  
	}
	printf("等待線程結束\n");
	for (i=0; i<threadnum; i++)
	{
		//CloseHandle( hThread[i] );
	}
	Singelton::releaseSingelton();
	cout <<"hello...."<<endl;
	system("pause");
	return 0;
}
           

 多線程下懶漢式單例的Double-Checked Locking優化

/*
建立MFC對話框應用程式。
友善使用臨界區類對象,同步線程
*/
// MFC Diagram 應用程式
#include "stdafx.h"
#include "01單例優化.h"
#include "01單例優化Dlg.h"
#include "afxdialogex.h"

#include "iostream"
using namespace std;

//臨界區
static CCriticalSection cs;
//man pthread_create() 
class Singleton
{
private:
	Singleton()
	{
		TRACE("Singleton begin\n");
		Sleep(1000);
		TRACE("Singleton end\n");

	}
	Singleton(const Singleton &);
	Singleton& operator = (const Singleton &);

public:
	static void printV()
	{
		TRACE("printV..\n");
	}

//請思考;懶漢式的Double-Check是一個經典問題!為什麼需要2次檢查 “if(pInstance == NULL)”
場景:假設有線程1、線程2、線程3,同時資源競争。
//1)第1個、2個、3個線程執行第一個檢查,都有可能進入黃色區域(臨界區)
//2)若第1個線程進入到臨界區,第2個、第3個線程需要等待
//3)第1個線程執行完畢,cs.unlock()後,第2個、第3個線程要競争執行臨界區代碼。
//4)假若第2個線程進入臨界區,此時第2個線程需要再次判斷 if(pInstance == NULL)”,若第一個線程已經建立執行個體;第2個線程就不需要再次建立了。保證了單例;
//5)同樣道理,若第2個線程,cs.unlock()後,第3個線程會競争執行臨界區代碼;此時第3個線程需要再次判斷 if(pInstance == NULL)。通過檢查發現執行個體已經new出來,就不需要再次建立;保證了單例。

	static Singleton *Instantialize()
	{
		if(pInstance == NULL)  //double check 
		{   
			cs.Lock(); //隻有當pInstance等于null時,才開始使用加鎖機制 二次檢查
			if(pInstance == NULL)
			{
				pInstance = new Singleton(); 
			}
			cs.Unlock();
		}
		return pInstance;
	}
	static Singleton *pInstance;
	
};

Singleton* Singleton::pInstance = 0;

void CMy01單例優化Dlg::OnBnClickedButton1()
{
	CCriticalSection  cs;
	cs.Lock();

	cs.Unlock();
	// TODO: 在此添加控件通知處理程式代碼
}

void threadfunc(void *myIpAdd)
{
	int id = GetCurrentThreadId();
	TRACE("\n threadfunc%d \n", id);
	Singleton::Instantialize()->printV();
	//Singelton::getSingelton()->pirntS();
}

void CMy01單例優化Dlg::OnBnClickedButton2()
{
	int i = 0; 
	DWORD dwThreadId[201], dwThrdParam = 1;
	HANDLE hThread[201]; 
	int threadnum = 3;

	for (i=0; i<threadnum; i++)
	{
		//hThread[i] = (HANDLE)_beginthreadex( NULL, 0, &threadfunc, NULL, 0,&dwThreadId[i] );
		hThread[i] = (HANDLE)_beginthread(&threadfunc, 0 , 0 );
		if (hThread[i] == NULL)
		{
			TRACE("begin thread %d error!!!\n", i);
			break;
		}		
	}

	for (i=0; i<threadnum; i++)
	{
		WaitForSingleObject( hThread[i], INFINITE );	
	}
	TRACE("等待線程結束\n");
	for (i=0; i<threadnum; i++)
	{
		//CloseHandle( hThread[i] );
	}
	//Singelton::releaseSingelton();
	
	TRACE("ddddd\n");
}
           

程式并發機制擴充閱讀

程式的并發執行往往帶來與時間有關的錯誤,甚至引發災難性的後果。這需要引入同步機制。使用多程序與多線程時,有時需要協同兩種或多種動作,此過程就稱同步(Synchronization)。引入同步機制的第一個原因是為了控制線程之間的資源同步通路,因為多個線程在共享資源時如果發生通路沖突通常會帶來不正确的後果。例如,一個線程正在更新一個結構,同時另一個線程正試圖讀取同一個結構。結果,我們将無法得知所讀取的資料是新的還是舊的,或者是二者的混合。第二個原因是

有時要求確定線程之間的動作以指定的次序發生,如一個線程需要等待由另外一個線程所引起的事件。

為了在多線程程式中解決同步問題,Windows提供了四種主要的同步對象,每種對象相對于線程有兩種狀态——信号狀态(signal state)和非信号狀态(nonsignalstate)。當相關聯的同步對象處于信号狀态時,線程可以執行(通路共享資源),反

之必須等待。這四種同步對象是:

(1)事件對象(Event)。事件對象作為标志線上程間傳遞信号。一個或多個線程可等待一個事件對象,當指定的事件發生時,事件對象通知等待線程可以開始執行。它有兩種類型:自動重置(auto-reset)事件和手動重置(manual-reset)事件。

(2)臨界區(Critical Section)。臨界區對象通過提供一個程序内所有線程必須共享的對象來控制線程。隻有擁有那個對象的線程可以通路保護資源。在另一個線程可以通路該資源之前,前一個線程必須釋放臨界區對象,以便新的線程可以索取對象的通路權。

(3)互斥量(Mutex Semaphore)。互斥量的工作方式非常類似于臨界區,隻是互斥量不僅保護一個程序内為多個線程使用的共享資源,而且還可以保護系統中兩個或多個程序之間的的共享資源。

(4)信号量(Semaphore)。信号量可以允許一個或有限個線程通路共享資源。它是通過計數器來實作的,初始化時賦予計數器以可用資源數,當将信号量提供給一個線程時,計數器的值減1,當一個線程釋放它時,計數器值加1。當計數器值小于等于0時,相應線程必須等待。信号量是Windows98同步系統的核心。從本質上講,互斥量是信号量的一種特殊形式。Windows/NT還提供了另外一種Windows95沒有的同步對象:可等待定時器(Waitable Timer)。它可以封鎖線程的執行,直到到達某一具體時間。這可以用于背景任務。

C/C++設計模式(一) 單例模式

關注公衆号:《碼之有道》,一起聊遊戲全棧開發!

1、公衆号回複:【教程】擷取零基礎遊戲開發用戶端+服務端全套教程。

2、公衆号回複:【實戰】擷取企業級實戰項目。

3、公衆号回複:【資料】擷取大學四年整理的所有自學資料。