用 C 语言编写设计模式--单例模式 (log日志文件的实现)

C语言实现设计模式的往期回顾:

    1. 用 C 语言实现简单工厂模式!

    2. 用 C 语言编写建造者模式!

    3. 用C语言实现原型模式!

    4. 用 C 语言实现一个静态代理模式 !

    5. C语言实现设计模式--装饰模式!

    6. 用C语言实现适配器模式!

    7. 用 C 语言编写设计模式--模板模式

    8. 用 C 语言实现有限状态机 FSM--基于表驱动

    9.用 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左右。

    日志文件中的内容:有不同等级的日志信息,也有不同线程写入的日志信息。

工程使用:

  1. 工程目录:log目录存放日志文件

  2. 再Linux平台下,输入make进行编译,输入make clean清除编译中间文件。mainApp是可执行文件。

  3. 在singleton.h文件有两个宏的定义,设置日志文件大小,即日志文件名字前缀。

    #define LOG_FILE_SIZE (10*1024U)
    #define LOG_FILE_NAME "./log/test"

👀

六、总结​​​​​​​

    虽然C语言是面向过程的编程语言,但是我们在设计程序的时候,可以考虑用面向对象的方式去设计,这样提高我们程序的“高内聚、低耦合”特性,便于维护。

想要完整设计模式代码的小伙伴:在微信公众号【Linux编程用C】后台回复    designer   即可获取,不断更新中!

第一个文件夹是此处实现的单例模式代码。

这个是交流群,欢迎大家扫描加入,一起分享学习!

(注:如果二维码过期了请添加小C微信号:LinuxCodeUseC 拉入群聊)

PS:若大家想看C语言版本的其他设计模式,

请大家 点赞! 转发!关注吧!~~

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值