背景
在23种设计模式中,单例模式属于创建型模式,在面向对象设计里是最简单的一种设计模式。
名词释义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
C语言应用
说白了就是有创建一个唯一变量实体,全局共享。
例子
这个模式在单片机底层驱动会用得比较多,比如ST官方的HAL库,基本就是一个外设定义一个实体。下面我们就以串口通信为例,来看下这个模式有什么特点。
根据创建实体的位置,这里分为懒汉式和饿汉式两种。
懒汉式
顾名思义,很懒,只有在使用的时候才创建实体。建议在存在内存管理的系统中使用。
#include <stdint.h>
#include <stdlib.h>
/* 串口通信缓存大小 */
#define UART_BUFF_SIZE 255
/* 串口通信结构块 */
struct tagUartCB
{
uint8_t Buff[UART_BUFF_SIZE];
uint8_t Size;
};
/* 获取实体 */
struct tagUartCB* GetUartCB(void)
{
static struct tagUartCB* uart_cb = NULL;
if (NULL == uart_cb)
{
/* --此处可能被重入-- */
uart_cb = malloc(sizeof(struct tagUartCB));
}
return uart_cb ;
}
饿汉式
就如名字所说,饿汉,巴不得马上就可以使用,所以程序开始运行时即已创建实体。模块内部创建实体,对外只提供一个调用函数。
#include <stdint.h>
#include <stdlib.h>
/* 串口通信缓存大小 */
#define UART_BUFF_SIZE 255
/* 串口通信结构块 */
struct tagUartCB
{
uint8_t Buff[UART_BUFF_SIZE];
uint8_t Size;
};
/* 唯一实体 */
static struct tagUartCB UartCB;
/* 获取实体 */
struct tagUartCB* GetUartCB(void)
{
return UartCB;
}
线程锁
在使用懒汉式的时候,会存在一个问题,就是在多线程调用时,可能会导致重入,从而导致创建了多个实体,为了解决这个问题,这里可以加个线程锁。
#include <stdint.h>
#include <stdlib.h>
/* 串口通信缓存大小 */
#define UART_BUFF_SIZE 255
/* 串口通信结构块 */
struct tagUartCB
{
uint8_t Buff[UART_BUFF_SIZE];
uint8_t Size;
};
/* 获取实体 */
struct tagUartCB* GetUartCB(void)
{
static struct tagUartCB* uart_cb = NULL;
/* 禁止任务调度 */
xTaskResumeAll();
if (NULL == uart_cb)
{
uart_cb = malloc(sizeof(struct tagUartCB));
}
/* 启用任务调度 */
vTaskSuspendAll();
return uart_cb ;
}
因为一直调用禁止任务调度会影响其他任务的运行时效性,所以为了不影响正常使用的性能,上面代码可以改也如下方式。
/* 获取实体 */
struct tagUartCB* GetUartCB(void)
{
static struct tagUartCB* uart_cb = NULL;
/* 已创建了实体就不禁止任务调度 */
if (NULL == uart_cb)
{
/* 禁止任务调度 */
xTaskResumeAll();
if (NULL == uart_cb)
{
uart_cb = malloc(sizeof(struct tagUartCB));
}
/* 启用任务调度 */
vTaskSuspendAll();
}
return uart_cb ;
}
适用范围
一般在驱动中最为常见,比如现在有一个板子,上面有两个485通信,如果此时要做串口的通信缓存,那只要做两套缓存即可,即使逻辑层面上,两个485可以跟多个设备进行通信,但每个485同一时刻只能跟一个设备通信,即同一时刻只需要一套缓存,那么此时多个设备的通信缓存是可以共享的。这时候使用单例模式可以大量减少内存的消耗。
优势
- 节省Ram空间
- 资源共享,不需要考虑同步问题
劣势
- 共用全局资源,多线程使用时需要考虑重入的风险。