C语言回调日志库的实现

C语言回调日志库的实现

Author: ChrisZZ
Link: https://www.cnblogs.com/zjutzz
Time 2024-05-04 00:00:15

0. 目的

参照 https://github.com/rxi/log.c 这一日志库,模仿实现. 为实际业务中的日志库提供参考思路。

不用 C++ 实现日志库, 因为业务中各个模块是 C API,不强制各个模块都用 C++(公司没有高密度、高水平的C++算法工程师)。C简洁够用。

本篇不涉及 ASCII 转义字符的显示。

1. 打印文件名、行号

__FILE__ 在预编译阶段自动展开为文件名。

__LINE__ 自动在编译阶段展开为行号。

__FUNCTION__ 自动在编译阶段展开为函数名,但可能会暴露业务敏感信息,一般不用, 或者考虑增加编译期字符串加密技术,尚未实践。

#define LOG_DEBUG1(msg) log_log1(__FILE__, __LINE__, msg)
void log_log1(const char* file, int line, const char* msg)
{
    printf("%s:%d %s", file, line, msg);
}

2. 打印时间

localtime() 获取时间, strftime() 修改显示格式。

localtime 不是线程安全的, 多线程场景使用时候要加锁保护,作为临界区的一部分。

#nclude <time.h>

// get current time
time_t t = time(NULL);
struct tm* time = localtime(&t);

// convert it to human-readable format
char time_str[32];
size_t count = strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", time);
time_str[count] = '\0';

// print the time str
printf("current time: %s\n", time_str);

3. 打印不定参数

类似于 printf 函数支持不定参数:先给定格式串, 再给出每个格式的替代值。

不直接用 printf 是为了同时保持打印文件名、行号、时间等信息。

使用 va_list, va_start, va_end 来解析不定参数, 使用 vprintf 来执行 printf 风格参数的打印。

#define LOG_DEBUG2(fmt, ...) log_log2(fmt, __VA_ARGS__)
void log_log2(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);

    vprintf(fmt, args);

    va_end(args);
}

4. 获取时间:线程安全方式

使用 pthread API 里的 mutex 来保护 localtime() 的调用。 临界区尽可能最小化, 后续执行 strftime 并不是临界区一部分。

#define LOG_DEBUG4(fmt, ...) log_log4(__FILE__, __LINE__, fmt, __VA_ARGS__)
void log_log4(const char* file, int line, const char* fmt, ...)
{
    time_t t = time(NULL);
    pthread_mutex_lock(&lock);
    struct tm* time = localtime(&t);
    pthread_mutex_unlock(&lock);

    char time_str[32];
    size_t count = strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", time);
    time_str[count] = '\0';

    printf("%s %s:%d ", time_str, file, line);

    va_list args;
    va_start(args, fmt);
    vprintf(fmt, args);
    va_end(args);
}

5. 保持线程打印不被截断

两个线程同时向控制台打印 log, 每个线程的打印可能没结束就被另个线程截断, 原因是 printf()vprintf() 没有放入临界区。

依然使用 pthread API, 这次增加 mutex 的保护范围。

// 让每个线程的打印是独立而完整, 避免被截断
#define LOG_DEBUG5(fmt, ...) log_log5(__FILE__, __LINE__, fmt, __VA_ARGS__)
void log_log5(const char* file, int line, const char* fmt, ...)
{
    pthread_mutex_lock(&lock);

    time_t t = time(NULL);
    struct tm* time = localtime(&t);

    char time_str[32];
    size_t count = strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", time);
    time_str[count] = '\0';

    printf("%s %s:%d ", time_str, file, line);

    va_list args;
    va_start(args, fmt);
    vprintf(fmt, args);
    va_end(args);

    pthread_mutex_unlock(&lock);
}

6. 泛化: 回调方式传入打印函数

还记得用的 printf 函数吗? 也许应该把打印动作交给调用者, 他:

  • 可以增加打印的内容,例如时区,hostname
  • 可以删除打印,减小系统负载,或屏蔽算法日志内容,不提供给客户

关键代码:

// 用于打印的回调函数的类型
typedef void (PrintFn)(LogMessage* msg);

typedef struct LogContext
{
    LockFn* lock; // 锁函数
    pthread_mutex_t* mutex; // 互斥量
    PrintFn* print;   // 用于打印的回调函数
} LogContext;

LogContext g_ctx;   // 全局上下文

// 注册回调函数的函数
void log_add_print_fn(PrintFn* print)
{
    g_ctx.print = print;
}

#define LOG_DEBUG(fmt, ...) log_log(__FILE__, __LINE__, fmt, __VA_ARGS__)
void log_log(const char* file, int line, const char* fmt, ...)
{
    if (g_ctx.lock)
    {
        g_ctx.lock(true, g_ctx.mutex);
    }

    //printf("%s %s:%d ", g_loginfo.time_str, g_loginfo.file, g_loginfo.line);
    if (g_ctx.print)
    {
        LogMessage msg;
        msg.file = file;
        msg.line = line;
        msg.fmt = fmt;

        time_t t = time(NULL);
        struct tm* time = localtime(&t);
        size_t count = strftime(msg.time_str, sizeof(msg.time_str), "%Y-%m-%d %H:%M:%S", time);
        msg.time_str[count] = '\0';

        va_start(msg.ap, fmt);
        g_ctx.print(&msg);
        va_end(msg.ap);
    }

    if (g_ctx.lock)
    {
        g_ctx.lock(false, g_ctx.mutex);
    }
}

7. 泛化: 回调方式传入 lock 函数

日志库尽可能的支持更广泛的平台, 不能拘泥于 pthread API, 也应当支持 MSVC 多线程。 支持的形式是定义出接口,而不是把具体的 MSVC 多线程的实现耦合进来。

对於单线程场景, 加锁反而影响性能, 此时锁函数传入NULL值即可。

先定义锁函数的接口:

// 锁函数。true 为锁住,false 为开锁
typedef void (LockFn)(bool lock, pthread_mutex_t* mutex);

// 注册锁函数和互斥量
void log_set_lock(LockFn* lock, pthread_mutex_t* mutex)
{
    g_ctx.lock = lock;
    g_ctx.mutex = mutex;
}

不够彻底, pthread_mutex_t 也应该被重构为接口而非具体实现,继续修改得到:

typedef void (LockFn)(bool lock, void* udata);

typedef struct LogContext
{
    LockFn* lock;   // 锁函数
    void* udata;    // 被 lock 锁住的数据, 例如 pthread_mutex_t*
    PrintFn* print; // 用于打印的回调函数
} LogContext;

void log_set_lock(LockFn* lock, void* udata)
{
    g_ctx.lock = lock;
    g_ctx.udata = udata;
}

这一泛化版本,使得具体的 lockFn 可以有不同实现,调用者自行提供即可,包括而不限于:

  • pthread API 实现的 lock
  • msvc 多线程 API 实现的 lock
  • C++11 多线程 API 实现的 lock

8. all together: 一个支持回调的、支持线程安全的日志库实现

#if _MSC_VER
#define _CRT_SECURE_NO_WARNINGS
#endif

#include <stdio.h>
#include <stdarg.h>
#include <time.h>
#include <stdbool.h>

typedef struct LogMessage
{
    const char* file;   // 文件名
    int line;           // 行号
    const char* fmt;    // 格式串
    va_list ap;         // 格式串的不定参数
    char time_str[32];  // 时间
} LogMessage;

// 用于打印的回调函数的类型
typedef void (PrintFn)(LogMessage* msg);

// 锁函数。true 为锁住,false 为开锁
typedef void (LockFn)(bool lock, void* udata);

typedef struct LogContext
{
    LockFn* lock;   // 锁函数
    void* udata;    // 被 lock 锁住的数据, 例如 pthread_mutex_t*
    PrintFn* print; // 用于打印的回调函数
} LogContext;

LogContext g_ctx;   // 全局上下文

// 注册回调函数的函数
void log_add_print_fn(PrintFn* print)
{
    g_ctx.print = print;
}

// 注册锁函数和互斥量
void log_set_lock(LockFn* lock, void* udata)
{
    g_ctx.lock = lock;
    g_ctx.udata = udata;
}

// 使用标准输出执行打印的函数
void stdout_print_fn(LogMessage* msg)
{
    printf("%s %s:%d ", msg->time_str, msg->file, msg->line);
    vprintf(msg->fmt, msg->ap);
}

#define LOG_DEBUG(fmt, ...) log_log(__FILE__, __LINE__, fmt, __VA_ARGS__)
void log_log(const char* file, int line, const char* fmt, ...)
{
    if (g_ctx.lock)
    {
        g_ctx.lock(true, g_ctx.udata);
    }

    //printf("%s %s:%d ", g_loginfo.time_str, g_loginfo.file, g_loginfo.line);
    if (g_ctx.print)
    {
        LogMessage msg;
        msg.file = file;
        msg.line = line;
        msg.fmt = fmt;

        time_t t = time(NULL);
        struct tm* time = localtime(&t);
        size_t count = strftime(msg.time_str, sizeof(msg.time_str), "%Y-%m-%d %H:%M:%S", time);
        msg.time_str[count] = '\0';

        va_start(msg.ap, fmt);
        g_ctx.print(&msg);
        va_end(msg.ap);
    }

    if (g_ctx.lock)
    {
        g_ctx.lock(false, g_ctx.udata);
    }
}

#if __GNUC__
#include <pthread.h>
#include <unistd.h>

// 使用 pthread 的 mutex 来实现锁函数
void pthread_lock_fn(bool lock, void* mutex)
{
    if (lock)
    {
        pthread_mutex_lock((pthread_mutex_t*)mutex);
    }
    else
    {
        pthread_mutex_unlock((pthread_mutex_t*)mutex);
    }
}

// 使用 LOG_DEBUG 的示例线程函数
void* f(void* args)
{
    pthread_t thread_id = pthread_self();
    int* mode = (int*)args;
    for (int i = 0; i < 100; i++)
    {
        int j = (*mode) * 100 + i;
        LOG_DEBUG("hello %d in thread %lu\n", j, (unsigned long)thread_id);
    }
    return NULL;
}

int mutex_lock_example()
{
    // 定义局部的 mutex
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);

    // 注册回调函数. 如果不注册,则看不到任何输出
    log_add_print_fn(stdout_print_fn);

    // 设置锁, 如果不设置,在多线程时的打印会被截断,获取时间时会发生 data race
    log_set_lock(pthread_lock_fn, &mutex);

    pthread_t t1, t2;
    int mode1 = 1;
    pthread_create(&t1, NULL, f, &mode1);
    int mode2 = 2;
    pthread_create(&t2, NULL, f, &mode1);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    pthread_mutex_destroy(&mutex);

    return 0;
}
#endif

#if _MSC_VER

#include <windows.h>

void msvc_lock_fn(bool lock, void* critical_section)
{
    if (lock)
    {
        EnterCriticalSection((CRITICAL_SECTION*)critical_section);
    }
    else
    {
        LeaveCriticalSection((CRITICAL_SECTION*)critical_section);
    }
}

DWORD WINAPI f3(LPVOID lpParameter)
{
    int mode = *static_cast<int*>(lpParameter);
    DWORD thread_id = GetCurrentThreadId();
    for (int i = 0; i < 100; i++)
    {
        int j = mode * 100 + i;
        LOG_DEBUG("hello %d in thread %lu\n", j, (unsigned long)thread_id);
    }
    return 0;
}

int msvc_lock_example()
{
    CRITICAL_SECTION cs;
    InitializeCriticalSection(&cs);

    log_add_print_fn(stdout_print_fn);
    log_set_lock(msvc_lock_fn, &cs);

    int mode1 = 1;
    int mode2 = 2;

    HANDLE hThread1, hThread2;
    DWORD threadID1, threadID2;

    hThread1 = CreateThread(
        NULL,
        0,
        f3,
        &mode1,
        0,
        &threadID1
    );

    hThread2 = CreateThread(
        NULL,
        0,
        f3,
        &mode2,
        0,
        &threadID2
    );

    WaitForSingleObject(hThread1, INFINITE);
    WaitForSingleObject(hThread2, INFINITE);

    CloseHandle(hThread1);
    CloseHandle(hThread2);

    DeleteCriticalSection(&cs);
    return 0;
}
#endif

#define USE_CPP11 1

#if USE_CPP11
#include <mutex>
#include <thread>

void f2(int mode)
{
    std::thread::id thread_id = std::this_thread::get_id();
    for (int i = 0; i < 100; i++)
    {
        int j = mode * 100 + i;
        {
            LOG_DEBUG("hello %d in thread %lu\n", j, thread_id);
        }
    }
}

void cpp11_lock_fn(bool lock, void* mutex)
{
    if (lock)
    {
        static_cast<std::mutex*>(mutex)->lock();
    }
    else
    {
        static_cast<std::mutex*>(mutex)->unlock();
    }
}

int cpp11_lock_example()
{
    std::mutex mutex;

    log_add_print_fn(stdout_print_fn);
    log_set_lock(cpp11_lock_fn, &mutex);

    int mode1 = 1;
    std::thread t1(f2, mode1);

    int mode2 = 2;
    std::thread t2(f2, mode2);

    t1.join();
    t2.join();

    return 0;
}
#endif

int main()
{
#if __GNUC__
   mutex_lock_example();
#endif

#if _MSC_VER
    msvc_lock_example();
#endif

#if USE_CPP11
    cpp11_lock_example();
#endif

    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章