C语言实现设计模式的往期回顾:
3. 用C语言实现原型模式!
6. 用C语言实现适配器模式!
👀
一、什么是单例模式
单例模式是一种属于对象创建型的模式,即保证系统中类只生成一个对象。这个是一个十分常用的设计模式,比如系统中只初始化一次的资源,如 socket、log 日志文件等,能被多个线程访问,保证其有唯一 1 个实列存在,作为全局变量供其他地方访问,同时也避免重复资源被初始化操作带来错误。简而言之,就是对一个一次性资源的封装,避免被重复申请或初始化,然后作为一个全局变量被访问。
单例模式有如下几个特点:1.确保该资源只被申请或初始化一次 2.定义静态对象指针 3.提供一个函数,全局访问
👀
二、单例模式区别:懒汉模式与饿汉模式
单例模式又分为懒汉模式和饿汉模式,这两个模式的区别主要在于创建对象方式不一样。
饿汉模式 :定义静态对象指针时候就为它分配资源,由于语言特性限制,在 C++、Java 等语言中可以实现饿汉模式,但在 C 语言中不能实现。所以只能实现下述懒汉单例模式。
//singleton.h
//定义对象进行封装
typedef struct Singleton{
//....
}Singleton;
//创建静态对象指针
//创建对象,在C中此处不能实现,不能在外面直接调用函数创建对象
static Singleton* obj = 构造函数();
//定义函数返回对象指针,供外部程序使用
Singleton* get_inst(void)
{
return obj;
}
懒汉模式 : 与饿汉模式对比,即调用 get_inst()时候判断是否创建创建对象。如果未创建,就创建对象。区别就在此,其他的基本上都一样。
//singleton.h
//定义对象进行封装
typedef struct Singleton{
//....
}Singleton;
//创建静态对象指针
static Singleton* obj = NULL;
//定义函数返回对象指针,供外部程序使用
Singleton* get_inst(void)
{
if(obj==NULL){
//C语言中只能在函数内部调用创建对象
obj = 构造函数(); //创建对象
}
return obj;
}
👀
三、懒汉单例模式的线程安全
//定义函数返回对象指针,供外部程序使用
Singleton* get_inst(void)
{
if(obj==NULL){
obj = 构造函数(); //创建对象
}
return obj;
}
懒汉单例模式存在多线程竞争问题,当线程 A 调用 get_inst()时候,若第一次调用,obj 还未申请资源,便会调用构造函数创建,而此时线程 B 也调用 get_inst()时,若构造函数内部创建时间长,此时 obj 还是空,因此线程 B 也会调用构造函数创建对象,此时系统调用了两次构造函数(例如 socket 等创建了两次),不满足逻辑设计,因此需要考虑线程竞争。
第一种解决方案:直接加锁,如下示例,锁放在判断条件外,这种方式每次调用 get_inst()都存在加锁解锁,频繁的锁操作增加资源开销,降低了效率;锁放在判断条件内,存在竞争并发问题,几个线程第一次调用 get_inst(),第一次都会判断为空,都会进入 if 条件了,这种时候即使加锁,也没有用了,都会多次创建对象,只是创建对象的先后时间不同,造成内存泄漏,逻辑不正确。
//锁放在判断之外
Singleton* get_inst(void)
{
pthread_mutex_lock(&mutex);
if(obj==NULL){
obj = 构造函数(); //创建对象
}
pthread_mutex_lock(&mutex);
return obj;
}
//锁放在判断内
Singleton* get_inst(void)
{
if(obj==NULL){
pthread_mutex_lock(&mutex);
obj = 构造函数(); //创建对象
pthread_mutex_lock(&mutex);
}
return obj;
}
第二种解决方案:双检锁,即两次判断,再加锁。第一次判断若未空,再进去加锁,再为空,才创建对象。本篇中采用这种设计。
Singleton* get_inst(void)
{
if(obj==NULL){
pthread_mutex_lock(&mutex);
if(obj==NULL) {
obj = 构造函数(); //创建对象
}
pthread_mutex_lock(&mutex);
}
return obj;
}
👀
四、懒汉单例模式的实现
接下来我们将通过一个例子来实现懒汉单例模式,以平时开发中经常使用到的日志文件为例,一个程序系统中,会存在一个日志操作,通过记录程序的运行状态,方便我们根据日志文件进行程序 bug 的分析。定义一个 log 日志对象,实现不同等级的日志记录,并且保证系统中存在唯一一个 log 日志对象。外部程序通过 log_get_inst()进行访问操作 log 对象。
typedef struct log_t //定义日志对象
{
int (*debug)(const char *__restrict __fmt, ...);
int (*warning)(const char *__restrict __fmt, ...);
int (*error)(const char *__restrict __fmt, ...);
void (*destroy)();
int log_size; //日志文件大小,超过这个值重新创建一个新文件
char log_name[128]; //日志文件名字
FILE *wfile; //文件操作符
}log_t;
static log_t* singleton_log = NULL; //定义的静态对象指针
log_t* log_get_inst(void); //通过对外访问的函数
构造函数:通过静态变量 inst_times 控制 log 只被调用一次,并且再.c 文件中使用 static 修改函数,不能被外部调用。
static log_t* construct_singleton_log(int size, const char* filename)
{
static int inst_times=0; //设置一个变量,确保只创建唯一log对象
if(inst_times!=0 || !filename || size<=0) return NULL;
log_t* obj = (log_t*)malloc(sizeof(log_t));
if(!obj) return NULL;
memset(obj, 0, sizeof(obj));
//创建日志文件
if(_open_file(obj)==-1) return NULL;
obj->log_size = size;
obj->debug = log_debug; //给指针函数赋值
obj->warning = log_warning;
obj->error = log_error;
obj->destroy = log_destroy;
inst_times++;
return obj;
}
提供对外的访问接口:提供宏定义 _REENTRANT 判断程序是否使用多线程,决定是否加锁。通过双检锁机制保证线程安全。
/**
* @brief: 懒汉单例模式, 供外部调用访问
* @return: 返回一个单例对象
*/
log_t* log_get_inst (void)
{
if(!singleton_log){ //双检锁
#ifdef _REENTRANT //是否使用多线程
pthread_mutex_lock(&log_mutex);
#endif
if(!singleton_log){
singleton_log = construct_singleton_log(LOG_FILE_SIZE, LOG_FILE_NAME);
}
#ifdef _REENTRANT //是否使用多线程
pthread_mutex_unlock(&log_mutex);
#endif
}
return singleton_log;
}
接下来时 log 日志文件功能函数的实现:buffer 即传入要写入日志文件的内容,通过静态变量 is_check 控制每 16 次检测日志文件是否达到设定最大值,是否创建新的日志文件,根据系统时间来创建日志的文件名字,这样当系统磁盘空间不足时候,可以根据时间手动去删除以前的日志。
/**
* @brief: 将日志内容写入文件
* @buffer: 日志内容
* @return: 0:ok -1:err
*/
static int _write_file(const char* buffer)
{
static unsigned char is_check=1;
struct stat log_fsta;
size_t ret=0;
if(!buffer || !singleton_log) return -1;
#ifdef _REENTRANT //是否使用多线程
pthread_mutex_lock(&log_mutex);
#endif
ret = fwrite(buffer, 1, strlen(buffer), singleton_log->wfile);
// if( (++is_check) % 16 != 0 ) goto exit; //写入16次检测一次是否要将日志写入另外一个文件
if( ((++is_check)&0xF) != 0 ) goto exit; //写入16次检测一次是否要将日志写入另外一个文件
fflush(singleton_log->wfile);
stat( singleton_log->log_name, &log_fsta ); //获取文件的大小
if( log_fsta.st_size > singleton_log->log_size ){ //写到设定值,重新打开一个文件写入
fclose( singleton_log->wfile ); //关闭当前文件
_open_file(singleton_log); //创建一个新文件
}
exit:
#ifdef _REENTRANT //是否使用多线程
pthread_mutex_unlock(&log_mutex);
#endif
return ret;
}
函数 debug、warning、error 不同等级的记录,实现几乎一样的,下面以 debug 为例:先对当前记录的时间、文件名字、行号格式化到 buffer 以后,再将其他信息格式化到 buffer,这样记录的信息就比较多,便于分析。
/**
* @brief: log记录debug的日志信息
* @input: 传入可变参数
* @note: 采用static修饰,外部其他文件不能直接调用,
* 赋值给函数指针,通过函数指针进行调用
* @return: 0:ok -1:err
*/
static int log_debug(const char *__restrict __fmt, ...)
{
va_list args;
time_t rawtime;
struct tm *tminfo=NULL;
int size=0, ret = 0;
char buffer[1024], time_buf[128];
if(!singleton_log) return -1;
time(&rawtime);
tminfo = localtime(&rawtime);
// strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tminfo);
size = sprintf(buffer, "[debug]: Time:%d-%d-%d %d:%d:%d File:%s Line:%d @: ",
tminfo->tm_year+1900, tminfo->tm_mon+1, tminfo->tm_mday, \
tminfo->tm_hour, tminfo->tm_min, tminfo->tm_sec, __FILE__, __LINE__);
va_start(args, __fmt); //定义可变参数列表
size = vsnprintf(buffer+size, 1024-size, __fmt, args); //buffer+size是偏移前面固定信息,避免被覆盖
va_end(args);
return _write_file(buffer);
}
👀
五、功能测试
测试函数如下:包含单例模式,直接调用debug、warning、error等记录日志信息。采用多线程测试,创建两个线程,都往同一个日志文件中写数据,当日志文件大小超过宏定义设置的大小时候,会根据系统时间创建新的日志文件。
#include "singleton.h"
#include <pthread.h>
void* task1(void *args)
{
int i=0;
for(i=0; i<100; i++){
log_get_inst()->debug("task1 i=%d....\n", i);
log_get_inst()->warning("task1 i=%d....\n", i);
log_get_inst()->error("task1 i=%d....\n", i);
usleep(500000);
}
}
void* task2(void *args)
{
int i=0;
for(i=0; i<100; i++){
log_get_inst()->debug("task2 i=%d....\n", i);
log_get_inst()->warning("task2 i=%d....\n", i);
log_get_inst()->error("task2 i=%d....\n", i);
usleep(500000);
}
}
int main(int argc, char **argv)
{
pthread_t th1, th2;
pthread_create(&th1, NULL, task1, NULL);
pthread_create(&th2, NULL, task2, NULL);
pthread_join(th1, NULL);
pthread_join(th2, NULL);
log_get_inst()->destroy();
return 0;
}
运行生成几个日志文件,实际使用中可以把文件阈值设置大一点,此处作为测试设置10k左右。
日志文件中的内容:有不同等级的日志信息,也有不同线程写入的日志信息。
工程使用:
-
工程目录:log目录存放日志文件
-
再Linux平台下,输入make进行编译,输入make clean清除编译中间文件。mainApp是可执行文件。
-
在singleton.h文件有两个宏的定义,设置日志文件大小,即日志文件名字前缀。
#define LOG_FILE_SIZE (10*1024U) #define LOG_FILE_NAME "./log/test"
👀
六、总结
虽然C语言是面向过程的编程语言,但是我们在设计程序的时候,可以考虑用面向对象的方式去设计,这样提高我们程序的“高内聚、低耦合”特性,便于维护。
想要完整设计模式代码的小伙伴:在微信公众号【Linux编程用C】后台回复 designer 即可获取,不断更新中!
第一个文件夹是此处实现的单例模式代码。
这个是交流群,欢迎大家扫描加入,一起分享学习!
(注:如果二维码过期了请添加小C微信号:LinuxCodeUseC 拉入群聊)
PS:若大家想看C语言版本的其他设计模式,
请大家 点赞! 转发!关注吧!~~