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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
2013-05-04 OpenCV条码(6)简单实现