设计模式学习(五):单例模式及其优化示例(C语言)

一、前言

单例模式(Singleton Pattern)是最简单的设计模式之一,因此并不为其专门开一次研讨会,在闲余时间自行学习,接下来我们来看看该模式的具体内容。、

二、单例模式

单例模式即保证一个类仅有一个实例,并提供一个访问它的全局访问点

单例模式本质上就是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可
以被创建(通过截取创建新对象的请求),并且它可以提供一个访问该实例的方法。

由此可见,单例模式主要是用来避免 一个全局使用的类被频繁地创建与销毁 的情况,当我们想控制实例数目或者节省系统资源的时候使用。

三、示例类图

在这里插入图片描述

单例模式类的构造函数需设置为私有,避免用户在外部调用,并提供一个公有的获取唯一实例的接口。

四、示例代码

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.h无变化,请查看4.1中的代码
//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.h无变化,请查看4.1中的代码
//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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容