天天看點

設計模式學習(五):單例模式及其優化示例(C語言)

目錄

    • 一、前言
    • 二、單例模式
    • 三、示例類圖
    • 四、示例代碼
      • 4.1 懶漢式(非線程安全)
      • 4.2 懶漢式+線程互斥鎖(線程安全)
      • 4.3 懶漢式+OpenMP并行程式設計(避免死鎖)
      • 4.4 餓漢式(非線程安全)
    • 五、總結
      • 5.1 單例模式的優缺點
      • 5.2 其他優化改進

一、前言

單例模式(Singleton Pattern)是最簡單的設計模式之一,是以并不為其專門開一次研讨會,在閑餘時間自行學習,接下來我們來看看該模式的具體内容。

二、單例模式

單例模式即保證一個類僅有一個執行個體,并提供一個通路它的全局通路點

單例模式本質上就是讓類自身負責儲存它的唯一執行個體。這個類可以保證沒有其他執行個體可

以被建立(通過截取建立新對象的請求),并且它可以提供一個通路該執行個體的方法。

由此可見,單例模式主要是用來避免 一個全局使用的類被頻繁地建立與銷毀 的情況,當我們想控制執行個體數目或者節省系統資源的時候使用。

三、示例類圖

設計模式學習(五):單例模式及其優化示例(C語言)

單例模式類的構造函數需設定為私有,避免使用者在外部調用,并提供一個公有的擷取唯一執行個體的接口。

四、示例代碼

4.1 懶漢式(非線程安全)

懶漢式的特點是延遲加載,比如配置檔案,采用懶漢式的方法,顧名思義,懶漢麼,很懶的,例如配置檔案的執行個體在用到的時候才會加載。

簡單了解即在第一次擷取類的執行個體時調用構造函數進行執行個體化。

首先來看看最基本的實作,這種實作最大的問題就是不支援多線程。當多個線程同時請求第一次擷取執行個體時可能會建立多個指向不同執行個體的指針。

這種實作通常用在不要求線程安全的情況,優點是節省記憶體(第一次擷取時才建立執行個體),并且運作效率高,但在多線程不能正常工作。代碼如下:

//singleton.h
#ifndef SINGLETON_H
#define SINGLETON_H

#include <stdio.h>
#include <stdlib.h>

typedef struct  _singleton_t {
	int data;
}singleton_t;

/**
* @method singleton
* 擷取唯一執行個體(getInstance接口)。
*
* @return {singleton_t*} 傳回singleton執行個體。
*/
singleton_t* singleton();

/**
* @method singleton_destroy
* 析構函數(銷毀singleton執行個體,釋放記憶體)。
*/
void singleton_destroy();

#endif /*SINGLETON_H*/
           
//singleton.c
#include "singleton.h"
#include <assert.h>

/* 全局靜态指針(指向唯一執行個體) */
static singleton_t* s_singleton = NULL;

static singleton_t* singleton_create() {
	s_singleton = (singleton_t*)malloc(sizeof(singleton_t));
	s_singleton->data = 0;
	return s_singleton;
}

singleton_t* singleton() {
	if (s_singleton == NULL) {
		singleton_create();  /* 調用構造函數執行個體化 */
	}
	assert(s_singleton != NULL);
	return s_singleton;
}

void singleton_destroy() {
	if (s_singleton != NULL) {
		free(s_singleton);
	}
}
           
//main.c
#include "singleton.h"

int main(int argc, const char* argv[]) {

	singleton_t* singleton1 = singleton();
	singleton1->data = 10;

	singleton_t* singleton2 = singleton();
	singleton2->data = 20;

	if (singleton1 == singleton2) {
		printf("singleton1 == singleton2\n");
	}
	printf("singleton1->data = %d\n", singleton1->data);
	printf("singleton2->data = %d\n", singleton2->data);

	singleton_destroy();
	system("pause");
	return 0;
}
           

輸入如下:

singleton1 == singleton2
singleton1->data = 20
singleton2->data = 20
           

4.2 懶漢式+線程互斥鎖(線程安全)

由于上述最基本的懶漢式單例模式無法在多線程的情況下正常工作,那麼對其進行進行優化,最簡單的方式就是加上互斥鎖,但這樣肯定會降低效率,代碼如下:

//singleton.c
#include "singleton.h"
#include <assert.h>
#include <pthread.h> 

extern pthread_mutex_t mute;

/* 全局靜态指針(指向唯一執行個體) */
static singleton_t* s_singleton = NULL;

static singleton_t* singleton_create() {
	s_singleton = (singleton_t*)malloc(sizeof(singleton_t));
	s_singleton->data = 0;
	return s_singleton;
}

singleton_t* singleton() {
	pthread_mutex_lock(&mute); /* 上鎖 */
	if (s_singleton == NULL) {
		singleton_create();  /* 調用構造函數執行個體化 */
	}
	pthread_mutex_unlock(&mute); /* 解鎖 */
	assert(s_singleton != NULL);
	return s_singleton;
}

void singleton_destroy() {
	if (s_singleton != NULL) {
		free(s_singleton);
	}
}
           
//main.c
#include <pthread.h> 
#include "singleton.h"

pthread_mutex_t mute; /* 互斥鎖 */
int main(int argc, const char* argv[]) {
    pthread_mutex_init(&mute, NULL); /* 初始化互斥鎖 */
    
	singleton_t* singleton1 = singleton();
	singleton_t* singleton2 = singleton();

	singleton1->data = 10;
	singleton2->data = 20;

	if (singleton1 == singleton2) {
		printf("singleton1 == singleton2\n");
	}
	printf("singleton1->data = %d\n", singleton1->data);
	printf("singleton2->data = %d\n", singleton2->data);

	singleton_destroy();
	pthread_mutex_destroy(&mute); /* 釋放互斥鎖 */
	
	system("pause");
	return 0;
}
           

4.3 懶漢式+OpenMP并行程式設計(避免死鎖)

使用OpenMP并行程式設計可以避免多個程序同時修改執行個體對象時造成不同程序之間互相等待進而導緻死鎖的情況,代碼如下:

關于OpenMP并行程式設計的内容可以參考部落格:https://www.cnblogs.com/hantan2008/p/5961312.html
//singleton.c
#include "singleton.h"
#include <assert.h>
#include <omp.h>

extern omp_lock_t lock;

/* 靜态全局指針(指向唯一執行個體) */
static singleton_t* s_singleton = NULL;

static singleton_t* singleton_create() {
	s_singleton = (singleton_t*)malloc(sizeof(singleton_t));
	s_singleton->data = 0;
	return s_singleton;
}

singleton_t* singleton() {
	omp_set_lock(&lock); /* 上omp鎖 */
	if (s_singleton == NULL) {
		singleton_create();  /* 調用構造函數執行個體化 */
	}
	omp_unset_lock(&lock); /* 解omp鎖 */
	assert(s_singleton != NULL);
	return s_singleton;
}

void singleton_destroy() {
	if (s_singleton != NULL) {
		free(s_singleton);
	}
}
           
//main.c
#include<omp.h>
#include "singleton.h"

omp_lock_t lock; /* omp鎖 */
int main(int argc, const char* argv[]) {
	omp_set_num_threads(20);
    omp_init_lock(&lock); /* 初始化omp鎖 */
    
	singleton_t* singleton1;
	singleton_t* singleton2;
#pragma omp parallel
	{
		singleton1 = singleton();
		singleton1->data = omp_get_thread_num();
	}
#pragma omp parallel 
	{
		singleton2 = singleton();
		singleton2->data = omp_get_thread_num();
	}

	if (singleton1 == singleton2) {
		printf("singleton1 == singleton2\n");
	}
	printf("singleton1->data = %d\n", singleton1->data);
	printf("singleton2->data = %d\n", singleton2->data);
	
    omp_destroy_lock(&lock); /* 釋放omp鎖 */
	singleton_destroy();
	system("pause");

	return 0;
}
           

4.4 餓漢式(非線程安全)

餓漢式的特點是程式一運作就建立執行個體了,因為餓漢式使用靜态局部變量讓類加載時就執行個體化,其優點是節省時間,但浪費記憶體。

如果說懶漢式是“時間換空間”,那麼餓漢式就是“空間換時間”,餓漢式通常在複雜類執行個體化時間較長時使用,代碼如下:

//singleton.h
#ifndef SINGLETON_H
#define SINGLETON_H

#include<stdio.h>
#include<stdlib.h>

typedef struct  _singleton_t {
	int data;
}singleton_t;

/**
* @method singleton
* 擷取唯一執行個體(getInstance接口)。
*
* @return {singleton_t*} 傳回singleton執行個體。
*/
singleton_t* singleton();

#endif /*SINGLETON_H*/
           
//singleton.c
#include "singleton.h"
#include <assert.h>

singleton_t* singleton() {
	static singleton_t s_singleton; /* 靜态局部對象(唯一執行個體) */
	assert(&s_singleton != NULL);
	return &s_singleton;
}
           
//main.c
#include "singleton.h"

int main(int argc, const char* argv[]) {

	singleton_t* singleton1 = singleton();
	singleton_t* singleton2 = singleton();

	singleton1->data = 10;
	singleton2->data = 20;

	if (singleton1 == singleton2) {
		printf("singleton1 == singleton2\n");
	}
	printf("singleton1->data = %d\n", singleton1->data);
	printf("singleton2->data = %d\n", singleton2->data);

	system("pause");
	return 0;
}
           

輸入如下:

singleton1 == singleton2
singleton1->data = 20
singleton2->data = 20
           

上述寫法對于多線程擷取執行個體是安全的,但若想要實作多線程修改執行個體對象,同樣需要添加互斥鎖,可參考本文 4.2 章節。

五、總結

首先注意一點,為了友善示範,以上示例代碼我沒有寫單例類 singleton_t 中的成員變量的get方法和set方法,通常情況下需要提供這些方法給使用者使用。

5.1 單例模式的優缺點

優點:

  1. 在記憶體裡隻有一個執行個體,減少了記憶體的開銷,尤其是頻繁的建立和銷毀執行個體(比如管理學院首頁頁面緩存)。
  2. 避免對資源的多重占用(比如寫檔案操作)。
  3. 對唯一執行個體的受控通路,它可以嚴格的控制客戶怎樣以及何時通路它。
  4. 縮小命名控件,單例模式是對全局變量的一種改進,它避免了那些儲存唯一執行個體的全局變量污染命名空間。

缺點:沒有接口,不能繼承,與單一職責原則沖突,一個類應該隻關心内部邏輯,而不關心外面怎麼樣來執行個體化。

5.2 其他優化改進

針對以上缺點,在C語言中其實是有解決方案的,可以将單例類(singleton_t)抽象出來作為基類,将其執行個體化的過程(構造函數)放到子類中。

比如 AWTK 源碼中實作的 視窗管理器(window_manager),基類 window_manager_t 采用了單例模式,執行個體化的過程放在其子類 window_manager_simple_t 中,通過外部注入的方式設定到靜态全局指針中(指向唯一的執行個體化對象),感興趣的朋友可以自行研究源碼,GitHub倉庫:https://github.com/zlgopen/awtk。

AWTK是 ZLG 開發的開源 GUI 引擎,官網位址:https://www.zlg.cn/index/pub/awtk.html。

繼續閱讀