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;
}