Implementing dprintf() without __VA_ARGS__
Posted in C/C++, Programming by jeffhung @ January 29th, 2008 |
本系列共有三篇文章,以及一篇补充数据,建议依照以下顺序阅读:
Race condition in C wrapper of mutex class (补充资料)
Implementing dprintf() with __VA_ARGS__
Implementing dprintf() without __VA_ARGS__
Implementing DFORMAT and DOUT (尚未完成)
在这篇《Implementing dprintf() with __VA_ARGS__》里,简介了如何利用 __VA_ARGS__ 做出好用的 dprintf(),以协助我们仅在测试版里倾印侦错讯息。然而,某些老旧但仍然十分活跃的 compilers,如 VC6,并没有支援 __VA_ARGS__。因为通常来说,为了照顾既有的大量程序,以及一些政治问题,更换 compiler 有时是不可能的。所以我们最好还是来想想,怎样在不支持 __VA_ARGS__ 的情况下,仍然将 dprintf() 实作出来。
仔细检视 dprintf_v2() 与 dprintf_v3() 在 #define 时的差别,我们会发现,dprintf_v2() 其实并不是一个真正的 function-like macro,而仅仅只是把 dprintf_v2 这个「名字」给代换成 dprintf_v2_impl。于是,原本跟在后面,属于 dprintf_v2 的参数,经过 preprocessor 处理之后,变成了跟在 dprintf_v2_impl 的后面。这样的作法,除了之前提过的,在释出版里,剩下来的参数列,不一定有办法被最佳化去掉之外,还有一个更大的问题就是,没有办法于呼叫 dprintf_v2() 时省略 __FILE__ 和 __LINE__,自动于转换成呼叫 dprintf_v2_impl() 时加上这两个参数。
也就是说,透过 __VA_ARGS__ 的帮助,我们就可以在 function-like macro 里,利用 ... 与 __VA_ARGS__,更精细地控制参数的对应关系。相反地,若是在没有 __VA_ARGS__ 的情况下,我们就只能做到 function 名字的代换,呼叫的参数列,必须原汁原味,无法更动。然而,我们却希望,能够自动嵌入 __FILE__ 与 __LINE__ 两个参数,而这就是不支持 __VA_ARGS__ 所产生的最大问题。
因此,如果我们能够找到某种方法,不利用 __VA_ARGS__,「夹带」__FILE__ 与 __LINE__ 使得真正印讯息得函式本体,能够得到这两个参数的话,我们就可以避掉这个问题。
使用 C++ 对象承载 __FILE__ 与 __LINE__ 信息:dprintf_v4.cpp
第一个窜入脑袋里的解法是,利用 C++ 的对象,承装 __FILE__ 与 __LINE__ 等额外的信息。也就是说,利用函数对象 (function object,或称 functor) 来实作,这一招在 C++ 是很常见的手法。如下:
#include <cstdio>
#include <cstdarg>
class dprintf_v4_impl
{
public:
dprintf_v4_impl(const char* src_file, size_t src_line)
: src_file_(src_file)
, src_line_(src_line)
{
}
void operator()(bool enable, const char* fmt, ...) const
{
va_list ap;
if (enable) {
fprintf(stderr, "%s (%d): ", src_file_, src_line_);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "/n");
fflush(stderr);
}
}
private:
const char* src_file_;
size_t src_line_;
};
#ifndef NDEBUG
# define dprintf_v4 dprintf_v4_impl(__FILE__, __LINE__)
#else
# define dprintf_v4
#endif
int main()
{
int enable = 1;
int i = 3;
dprintf_v4(enable, "i == %d", i);
return 0;
}
// OUTPUT:
// dprintf_v4.cpp (39): i == 3
首先,dprintf_v4 这个函数名称,会被代换成 dprintf_v4_impl(__FILE__, __LINE__),实际上 dprintf_v4_impl 是个 class,所以这会产生一个暂时对象,同时把 __FILE__ 与 __LINE__ 存在暂时对象里。因为 dprintf_v4_impl 不仅仅只是一个普通的对象,而更是一个 functor,所以在暂时对象 dprintf_v4_impl(__FILE__, __LINE__) 后面还可以再接上括号与里面的参数。
也就是说,main() 里呼叫的 dprintf_v4(),在经过 preprocessing 处理后,就会变成:
dprintf_v4_impl(__FILE__, __LINE__)(enable, "i == %d", i);
// |---------暂时存在的函式对象--------||--呼叫 operator()()--|
于是 void operator()(bool enable, const char* fmt, ...) 这个 member function 被呼叫,而里面就是原本 dprintf_v3_impl() 的内容[1]。
当然,我们也可以不要 overload operator(),而是用如 print() 这样的成员函式名称,就变成是 #define dprintf_v4 dprintf_v4(__FILE__, __LINE__).print。不过,这样就不屌了,哈。
2008-03-12 更新:刚刚看到的,这篇《Getting around the need for a vararg #define just to automatically use __FILE__ and __LINE__ in a TRACE macro》也提出了同样的技术。
承载 __FILE__ 与 __LINE__ 信息后回传 function pointer:dprintf_v5.c
然 而,dprintf_v4.cpp 使用 functor 的招式,是 C++ only,没有办法在 C 里面使用。在 C 里,没有「对象」这种东西,所以就不会有「隐藏的 this 指标[2]」,帮助我们将 __FILE__ 与 __LINE__ 信息,「运送」到最终印出的程序片段。因此,我们必须要想别的办法,来传送 __FILE__ 与 __LINE__ 这两个参数。
顺着 functor 的思路,我们可以找到 functor 在 C 里的对应:function pointer。如果我们先执行一个 function,将 __FILE__ 与 __LINE__ 存在「某个地方」,然后这个 function 回传一个 function pointer,经 preprocessor 代换之后,这个 function pointer 会接上 dprintf 的参数串执行之,此时再将 __FILE__ 与 __LINE__ 取回来。这样一来,就可以做到「不经过参数列的信息传送」。程序写出来大致如下:
#include <stdio.h>
#include <stdarg.h>
typedef void (*dprintf_v5_fn)(int enable, const char* fmt, ...);
struct file_line_t
{
const char* file;
size_t line;
};
// Global for transporting __FILE__ and __LINE__.
static struct file_line_t g_file_line;
void dprintf_v5_impl(int enable, const char* fmt, ...)
{
va_list ap;
if (enable) {
fprintf(stderr, "%s (%d): ", g_file_line.file, g_file_line.line);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "/n");
fflush(stderr);
}
}
dprintf_v5_fn dprintf_v5_front(const char* src_file, size_t src_line)
{
g_file_line.file = src_file;
g_file_line.line = src_line;
return &dprintf_v5_impl;
}
#ifndef NDEBUG
# define dprintf_v5 (*dprintf_v5_front(__FILE__, __LINE__))
#else
# define dprintf_v5
#endif
int main()
{
int enable = 1;
int i = 3;
dprintf_v5(enable, "i == %d", i);
return 0;
}
// OUTPUT:
// dprintf_v5.c (44): i == 3
首先,我们把 dprintf_v5 给 #define 成 (*dprintf_v5_front(__FILE__, __LINE__)),当展开 dprintf_v5 时,会先呼叫 dprintf_v5_front() 把 __FILE__ 与 __LINE__ 存在某个地方,然后 dprintf_v5_front() 会回传一个指向 dprintf_v5_impl() 的 function pointer。接着,呼叫这个 function pointer 所指到的函式,也就是 dprintf_v5_impl(),dprintf_v5_impl() 会将之前存着的 __FILE__ 与 __LINE__ 取出,最后将讯息顷印出来。
很不幸地,这里所谓的「某个地方」,是一个全域变量 g_file_line 变量,这也意味着,这样的写法,是 non-thread-safe 的。
加上 sleep 以验证共享错误:dprintf_v5mt.c
举 例来说,若 thread1 执行到了 dprintf_v5_front(),把 thread1.c 与 314 存进了 g_file_line,在 thread1 还没有执行 dprintf_v5_impl() 之前,thread2 也执行到了 dprintf_v5_front(),把 g_file_line 改成了 thread2.c 与 749,然后又切回 thread1 执行 dprintf_v5_impl(),此时就会印出错误的 file/line 信息。且看以下的范例:
#include <stddef.h>
#include <stdio.h>
#include <time.h>
#include <stdarg.h>
#include <pthread.h>
#define ENTER_FUNCTION() /
fprintf(stderr, "thread#%d: %s(): entering.../n", /
pthread_self(), __FUNCTION__); /
/**/
/** Sleep @p s seconds for current thread. */
void thread_sleep(size_t s)
{
fprintf(stderr, "thread#%d: sleep for %d seconds.../n", pthread_self(), s);
struct timespec ts = {0}; // reset to all zero
ts.tv_sec = s;
nanosleep(&ts, 0);
}
typedef void (*dprintf_v5mt_fn)(int enable, const char* fmt, ...);
struct file_line_t
{
const char* file;
size_t line;
};
static struct file_line_t g_file_line;
void dprintf_v5mt_impl(int enable, const char* fmt, ...)
{
ENTER_FUNCTION();
va_list ap;
if (enable) {
fprintf(stderr, "thread#%d: %s (%d): ",
pthread_self(), g_file_line.file, g_file_line.line);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "/n");
fflush(stderr);
}
}
dprintf_v5mt_fn dprintf_v5mt_front(const char* src_file, size_t src_line)
{
ENTER_FUNCTION();
g_file_line.file = src_file;
g_file_line.line = src_line;
thread_sleep(2);
return &dprintf_v5mt_impl;
}
#ifndef NDEBUG
# define dprintf_v5mt (*dprintf_v5mt_front(__FILE__, __LINE__))
#else
# define dprintf_v5mt
#endif
int g_enable = 1;
int g_data = 3;
void* main_thread(void* param)
{
ENTER_FUNCTION();
thread_sleep(1);
dprintf_v5mt(g_enable, "g_data == %d", g_data); // line 64
return 0;
}
void* other_thread(void* param)
{
ENTER_FUNCTION();
thread_sleep(2);
dprintf_v5mt(g_enable, "g_data == %d", g_data); // line 72
return 0;
}
int main()
{
pthread_t other_thr;
pthread_create(&other_thr, NULL, other_thread, NULL);
main_thread(0);
pthread_join(other_thr, NULL);
return 0;
}
// OUTPUT:
// thread#134557696: main_thread(): entering...
// thread#134558720: other_thread(): entering...
// thread#134557696: sleep for 1 seconds...
// thread#134558720: sleep for 2 seconds...
// thread#134557696: dprintf_v5mt_front(): entering...
// thread#134557696: sleep for 2 seconds...
// thread#134558720: dprintf_v5mt_front(): entering...
// thread#134558720: sleep for 2 seconds...
// thread#134557696: dprintf_v5mt_impl(): entering...
// thread#134557696: dprintf_v5mt.c (72): g_data == 3
// thread#134558720: dprintf_v5mt_impl(): entering...
// thread#134558720: dprintf_v5mt.c (72): g_data == 3
为 了追踪是哪个 thread 在做事,所以这个范例里的每一行程序,特地在前面多印出 thread id[3],thread#134557697 是 main_thread(),于第 64 行呼叫 dprintf_v5mt(),而 thread#134558720 是 other_thread(),于第 72 行呼叫 dprintf_v5mt()。利用故意加入的 thread_sleep(),从 output 我们看到,thread#134557697 先进入 dprintf_v5mt_front(),此时 g_file_line.line 应该是 64[4]。然而,在还没进入 dprintf_v5mt_impl() 之前,另外一个 thread#134558720 也进入了 dprintf_v5mt_front(),因此 g_file_line.line 被改掉了,所以最后印出来的行号,thread#134557696 与 thread#134558720 都显示成 72,而前者其实是错的。
使用 mutex 保护 __FILE__ 与 __LINE__ 信息:dprintf_v6mt.c
为 了解决这种共享的问题,理所当然的,要用 mutex 这种东西来保护共享信息。既然标题里提到了 VC6,但前面一直用 pthread 当范例,这样文不对题,是不对的行为。所以,在加入 mutex 机制的同时,我们一并让程序可以跨 win32/pthread:
#include <stddef.h>
#include <stdio.h>
#include <time.h>
#include <stdarg.h>
#if defined(WIN32)
# include <windows.h>
#else /* !defined(WIN32) */
# include <pthread.h>
#endif /* defined(WIN32) */
#if defined(WIN32)
# define thread_id() GetCurrentThreadId()
#else /* !defined(WIN32) */
# define thread_id() pthread_self()
#endif /* defined(WIN32) */
#define ENTER_FUNCTION() fprintf(stderr, "thread#%d: %s(): entering.../n", thread_id(), __FUNCTION__);
/** Sleep @p s seconds for current thread. */
void thread_sleep(size_t s)
{
fprintf(stderr, "thread#%d: sleep for %d seconds.../n", thread_id(), s);
struct timespec ts = {0};
ts.tv_sec = s;
nanosleep(&ts, 0);
}
/** Define mutex type according to current platform. */
#if defined(WIN32)
typedef CRITICAL_SECTION mutex_t;
#else /* !defined(WIN32) */
typedef pthread_mutex_t mutex_t;
#endif /* defined(WIN32) */
/** Initialize mutex object */
void mutex_init(mutex_t* mx)
{
#if defined(WIN32)
InitializeCriticalSection(mx);
#else /* !defined(WIN32) */
pthread_mutex_init(mx, NULL);
#endif /* defined(WIN32) */
}
/** Destroy mutex object */
void mutex_destroy(mutex_t* mx)
{
#if defined(WIN32)
DeleteCriticalSection(mx);
#else /* !defined(WIN32) */
pthread_mutex_destroy(mx);
#endif /* defined(WIN32) */
}
/** Lock mutex object */
void mutex_lock(mutex_t* mx)
{
#if defined(WIN32)
EnterCriticalSection(mx);
#else /* !defined(WIN32) */
pthread_mutex_lock(mx);
#endif /* defined(WIN32) */
}
/** Unlock mutex object */
void mutex_unlock(mutex_t* mx)
{
#if defined(WIN32)
LeaveCriticalSection(mx);
#else /* !defined(WIN32) */
pthread_mutex_unlock(mx);
#endif /* defined(WIN32) */
}
typedef void (*dprintf_v6mt_fn)(int enable, const char* fmt, ...);
struct file_line_t
{
const char* file;
size_t line;
};
static struct file_line_t g_file_line;
static mutex_t g_file_line_mutex; // to protect g_file_line
void dprintf_v6mt_impl(int enable, const char* fmt, ...)
{
ENTER_FUNCTION();
va_list ap;
if (enable) {
fprintf(stderr, "thread#%d: %s (%d): ", thread_id(), g_file_line.file, g_file_line.line);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "/n");
fflush(stderr);
}
mutex_unlock(&g_file_line_mutex);
}
dprintf_v6mt_fn dprintf_v6mt_front(const char* src_file, size_t src_line)
{
mutex_lock(&g_file_line_mutex);
ENTER_FUNCTION();
g_file_line.file = src_file;
g_file_line.line = src_line;
thread_sleep(2);
return &dprintf_v6mt_impl;
}
#ifndef NDEBUG
# define dprintf_v6mt (*dprintf_v6mt_front(__FILE__, __LINE__))
#else
# define dprintf_v6mt
#endif
int g_enable = 1;
int g_data = 3;
void* main_thread(void* param)
{
ENTER_FUNCTION();
thread_sleep(1);
dprintf_v6mt(g_enable, "g_data == %d", g_data); // line 124
return 0;
}
void* other_thread(void* param)
{
ENTER_FUNCTION();
thread_sleep(2);
dprintf_v6mt(g_enable, "g_data == %d", g_data); // line 132
return 0;
}
int main()
{
mutex_init(&g_file_line_mutex);
pthread_t other_thr;
pthread_create(&other_thr, NULL, other_thread, NULL);
main_thread(0);
pthread_join(other_thr, NULL);
mutex_destroy(&g_file_line_mutex);
return 0;
}
// OUTPUT:
// thread#134557696: main_thread(): entering...
// thread#134557696: sleep for 1 seconds...
// thread#134558720: other_thread(): entering...
// thread#134558720: sleep for 2 seconds...
// thread#134557696: dprintf_v6mt_front(): entering...
// thread#134557696: sleep for 2 seconds...
// thread#134557696: dprintf_v6mt_impl(): entering...
// thread#134557696: dprintf_v6mt.c (124): g_data == 3
// thread#134558720: dprintf_v6mt_front(): entering...
// thread#134558720: sleep for 2 seconds...
// thread#134558720: dprintf_v6mt_impl(): entering...
// thread#134558720: dprintf_v6mt.c (132): g_data == 3
从 程序的 output 我们可以看出,other_thread() 对 dprintf_v6mt() 的呼叫,被延迟了,所以 dprintf_v6mt_front() 一直到 main_thread() 的 dprintf_v6mt_impl() 把除错讯息印出来之后,才被 other_thread() 执行,所以最后两个 threads 所印出的程序代码行号,都是正确的。
然而我们 也可以感觉得到,整体程序的执行时间变长了,这是因为瓶颈在 g_file_line_mutex,若同时有很多个 thread 都要 lock 这个 mutex,大家就会锁在这边,造成效率的低落。当然,范例里因为故意加入了 thread_sleep(),所以效率的低落特别明显,实际上在跑的时候,不会有 thread_sleep(),所以对效率的影响,其实很小。由于测试版我们在乎的通常不是效率,因此这点 overhead,是可以被接受的。
C++ 之好,C 难以承受
基本上来说,dprintf_v6mt.c 已经是很好的解法了,然而,我们总是要尽量使用 C++ 的优点的,不是吗?:-p
为 了让 OS 提供的 thread 相关 API 更好用一些,一般我们会将 API 再包装过,提供一些更安全、易于管理的机制,组织成一套好用的 thread library。这里所谓的「OS 提供的 API」,指的是 pthread 与 Win32 SDK 里如 CreateThread()、_beginthreadex() 之类的函式。这个 thread library,可能有下面四种实作方式:
Implemented in C only, no extra C++ API.
Implemented in C++ only, no extra C API.
Implemented in C, with extra thin C++ wrappers.
Implemented in C++, with extra thin C wrappers.
为了要让这个 thread library 能够给纯 C 使用,又能够有 C++ 的 syntax candies[5],前两者基本上我们不考虑。
首先,就「Implemented in C, with extra thin C++ wrappers」这个部分,可能作法如下(基于 dprintf_v6mt.c,跨平台部分先忽略):
typedef CRITICAL_SECTION mutex_t;
void mutex_init(mutex_t* mx) { InitializeCriticalSection(mx); }
void mutex_destroy(mutex_t* mx) { DeleteCriticalSection(mx); }
void mutex_lock(mutex_t* mx) { EnterCriticalSection(mx); }
void mutex_unlock(mutex_t* mx) { LeaveCriticalSection(mx); }
/** Thin C++ wrappers for extra C++ syntax candies. */
class mutex
{
friend class mutex::guard; // let mutex::guard call lock/unlock().
public:
class guard
{
public:
guard(mutex& mx) : mx_(mx) { mx_.lock(); }
~guard() { mx_.unlock(); }
private:
mutex_& mx_; // reference to remember which mutex we locked.
};
mutex() { mutex_init(&mx_); }
~mutex() { mutex_destroy(&mx_); }
private:
void lock() { mutex_lock(&mx_); }
void unlock() { mutex_unlock(&mx_); }
mutex_t mx_;
};
在这个例子里,class mutex 实际上并没有真正做事,而是转而呼叫 C 版的函式[6]。不过,善用 C++ 的 RAII 的技巧可以让程序更安全,我们将 lock() 与 unlock() 藏在了 class mutex 的 private 区块里,然后额外提供了 class mutex::guard,用起来像这个样子:
class account { ... };
static map<string /* name */, account*> g_accounts; // global account table
static mutex g_accounts_mutex; // protect g_accounts.
void create_account(const char* name)
{
mutex::guard lock(g_accounts_mutex);
g_accounts.insert(make_pair(string(name), new account(name)));
}
如 pthread 就是典型的 C 接口,而 Boost.Thread,则是典型的 C++ 接口,前者容易成为跨系统通用的 API,后者则能善用 C++ 的好。一般而言,我们会希望使用功能更为强大,使用起来更不容易出错的 C++ 接口版。
然 而,mutex::guard 这个招式,却是没有办法应用在 dprintf_v6mt 上的。因为,mutex::guard 利用 constructor/destructor 执行 lock/unlock,虽然方便,但也限制住了 mutex::guard 的效果只及于同一个 block 中。然而,dprintf_v6mt 却需要在 dprintf_v6mt_front() 里 lock,在 dprintf_v6mt_impl() 里 unlock,不在同一个 block 里。是故,C++ 的好,在这个情境下,没办法给 C 接口的 function 使用。
因此,如果我们选择使用「Implemented in C, with extra thin C++ wrappers」的方式实作 thread library,在这里就无法使用好用的 C++ wrappers,只能直接使用 C implementation;而如果我们选择使用「Implemented in C++, with extra thin C wrappers」的方式实作,就必须改呼叫 thin C wrappers。
不过,就如同之前于《Race condition in C wrapper of mutex class》一文中探讨过的,想要拥有 C/C++ 双接口,有时候问题会更多。因此,最终我的选择会是,直接使用 C++ 实作 thread library,抛弃容易出问题的 C 接口。但也因为如此,我们就无法使用这个包好的 C++ thread library,必须直接在 dprintf_v6mt 里呼叫底层 OS 提供的 C APIs,并针对跨平台的需求,包出一组又一组,丑陋的 #ifdef,而这本来是该交由 thread library 解决的事。
避免使用 mutex 造成不必要的效能瓶颈:dprintf_v7mt.cpp
在 dprintf_v6mt.cpp 这个版本里,我们使用了 mutex 以确保暂时放在全域变量的 __FILE__ 和 __LINE__ 不会因为 race condition 而被其它 thread 改掉。这个作法其实不是那么地完美,因为 __FILE__ 与 __LINE__ 根本就是自己这个 thread 的数据,与其它 thread 没有关系,也就是说,不是共享的数据。但不需要共享的数据,却因为语言机制的缺乏,而不得不放在「大家一起用的空间(全域变量)」,所以导致必须额外付出 一个 mutex 的成本,当程序一复杂,总 thread 数一多起来的时候,大家就都会拥塞在这个 mutex 上,对效率影响甚巨。
所以, 要改进 dprintf_v6mt.cpp,我们就需要把 thread specific 的数据,改放到 thread 专属不与其它 thread 共享的空间,这样一来,就不需要用 mutex 锁住共享空间,继而造成效能瓶颈。对于一个 thread 来说,属于 thread 所独有的空间,除了 stack 之外,就剩 thread local storage (TLS),又称 thread specific data,前者是 Windows programming 的术语,后者是 pthread 的说法。使用 TLS 的方法,也是大同小异,基本上都是这四个接口:
步骤 Win32 Pthread
Allocate TLS space and obtain a key to access it TlsAlloc() pthread_key_create()
Set value to the space with the key TlsSetValue() pthread_setspecific()
Get value from the space with the key TlsGetValue() pthread_getspecific()
Deallocate TLS space with the key TlsFree() pthread_key_delete()
TLS 的使用有一些要注意的地方,尤其是考虑到跨平台的需求。这部份容后再专文介绍[7]。在这里我们就先使用 pthread 来实作:
#include <stddef.h>
#include <stdio.h>
#include <time.h>
#include <stdarg.h>
#include <stdlib.h>
#include <pthread.h>
#define ENTER_FUNCTION() /
fprintf(stderr, "thread#%d: %s(): entering.../n", /
pthread_self(), __FUNCTION__); /
/**/
/** Sleep @p s seconds for current thread. */
void thread_sleep(size_t s)
{
fprintf(stderr, "thread#%d: sleep for %d seconds.../n",
pthread_self(), s);
struct timespec ts = {0};
ts.tv_sec = s;
nanosleep(&ts, 0);
}
typedef void (*dprintf_v7mt_fn)(int enable, const char* fmt, ...);
#ifndef __cplusplus
typedef struct file_line_t file_line_t;
#endif
struct file_line_t
{
const char* file;
size_t line;
};
void free_file_line(void* value)
{
file_line_t* fl = (file_line_t*)value;
free(fl);
}
pthread_key_t g_file_line_key;
void dprintf_v7mt_impl(int enable, const char* fmt, ...)
{
ENTER_FUNCTION();
va_list ap;
if (enable) {
file_line_t* fl = (file_line_t*)pthread_getspecific(g_file_line_key);
fprintf(stderr, "thread#%d: %s (%d): ",
pthread_self(), fl->file, fl->line);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "/n");
fflush(stderr);
}
}
dprintf_v7mt_fn dprintf_v7mt_front(const char* src_file, size_t src_line)
{
ENTER_FUNCTION();
file_line_t* fl = (file_line_t*)pthread_getspecific(g_file_line_key);
if (fl == NULL) {
fl = (file_line_t*)malloc(sizeof(file_line_t));
pthread_setspecific(g_file_line_key, (void*)fl);
}
fl->file = src_file;
fl->line = src_line;
thread_sleep(2);
return &dprintf_v7mt_impl;
}
#ifndef NDEBUG
# define dprintf_v7mt (*dprintf_v7mt_front(__FILE__, __LINE__))
#else
# define dprintf_v7mt
#endif
int g_enable = 1;
int g_data = 3;
void* main_thread(void* param)
{
ENTER_FUNCTION();
thread_sleep(1);
dprintf_v7mt(g_enable, "g_data == %d", g_data); // line 124
return 0;
}
void* other_thread(void* param)
{
ENTER_FUNCTION();
thread_sleep(2);
dprintf_v7mt(g_enable, "g_data == %d", g_data); // line 132
return 0;
}
int main()
{
pthread_key_create(&g_file_line_key, &free_file_line);
pthread_t other_thr;
pthread_create(&other_thr, NULL, other_thread, NULL);
main_thread(0);
pthread_join(other_thr, NULL);
pthread_key_delete(g_file_line_key);
return 0;
}
// OUTPUT:
// thread#134557696: main_thread(): entering...
// thread#134557696: sleep for 1 seconds...
// thread#134558720: other_thread(): entering...
// thread#134558720: sleep for 2 seconds...
// thread#134557696: dprintf_v7mt_front(): entering...
// thread#134557696: sleep for 2 seconds...
// thread#134558720: dprintf_v7mt_front(): entering...
// thread#134558720: sleep for 2 seconds...
// thread#134557696: dprintf_v7mt_impl(): entering...
// thread#134557696: dprintf_v7mt.c (85): g_data == 3
// thread#134558720: dprintf_v7mt_impl(): entering...
// thread#134558720: dprintf_v7mt.c (93): g_data == 3
首 先,我们先建立一个 TLS 的 key,叫做 g_file_line_key,在 dprintf_v7mt_front() 里面,将 __FILE__ 与 __LINE__ 存在一个 malloc 出来的数据结构 file_line_t 里,然后把这个数据结构的地址,存在 g_file_line_key 所代表的 TLS 空间里。接着,在 dprintf_v7mt_impl() 里,自 TLS 里取出指向 file_line_t 结构的指针,得到当初存起来的 __FILE__ 与 __LINE__ 的值,印出。
因为当初在建立 g_file_line_key 时,已经指定使用 free_file_line() 销毁存在 TLS 里的值,所以我们可以不必顾虑该如何释放 file_line_t 结构所占用的空间。当 g_file_line_key 所对应的值改变,或 g_file_line_key 要被销毁时,或是 thread 结束时,free_file_line() 都会被呼叫。所以,我们只需要在 dprintf_v7mt_front() 里面检查 g_file_line_key 所对应的是不是 NULL,如果事的话,表示这个 thread 尚未建立 file_line_t 结构,便 malloc() 之。这样一来,就可以整个 thread 只在第一次时为 file_line_t 结构配置内存,然后从头用到尾。
由于是利用 TLS 承载 __FILE__ 与 __LINE__ 信息,纵使 g_file_line_key 是全域变量,但每个 thread 都会有自己的一份 file_line_t 结构,故 __FILE__ 与 __LINE__ 信息,并非是所有 threads 共享,故可以避免使用 mutex,继而避免效能的损失。
把所有东西组装起来:dprintf.cpp
至此,所有技术上的问题,大致都已经解决了,该取舍的部份,也都做出了选择。接下来,我们就可以把上面提到的所有技术,整合成一个完整的 dprintf() 实作。基本上,我们有以下几个可用的版本,他们的优缺点分别如下:
使用 __VA_ARGS__:没有 run-time overhead,但只有支持 C99 的编译器可以用[8]。
使用 C++ 对象:多了一个暂时对象,与 functor 的呼叫,但不会造成 multi-thread 执行的瓶颈。只能在 C++ 里使用。
使用 mutex 与 function pointer:多呼叫一个 dprintf_front(),且因使用 mutex 而会造成 multi-thread 执行的瓶颈。不限 C++,C 也可以使用。
使 用 TLS 与 function pointer:多呼叫一个 dprintf_front(),但因利用 TLS 故不会有 mutex 造成 multi-thread 执行的瓶颈,但理论上仍然会比使用 C++ 对象还要来得慢,端视 TLS 的实作而定。不限 C++,C 也可以使用。
基本上,我们可以使用 TLS 而抛弃 mutex 法。整个取舍的逻辑是:
if (支援 C99) {
使用 __VA_ARGS__
} else {
if (is C++) {
使用 C++ 对象
} else {
使用 TLS 搭配 function pointer
}
}
故,整套组装起来的 dprintf 实作应为 (仅实作 pthread 版):
用来辅助验证的 thread_tool.h:
#ifndef THREAD_TOOL_H_INCLUDED
#define THREAD_TOOL_H_INCLUDED
#include <stdio.h>
#include <stddef.h>
#if defined(__cplusplus)
extern "C" {
#endif
#define ENTER_FUNCTION() /
fprintf(stderr, "thread#%d: %s(): entering.../n", /
pthread_self(), __FUNCTION__); /
/**/
/** Sleep @p s seconds for current thread. */
void thread_sleep(size_t s);
#if defined(__cplusplus)
} // extern "C"
#endif
#endif /* THREAD_TOOL_H_INCLUDED */
与其实作 thread_tool.c,实际上线的系统,不需要用到这里面的东西:
#include "thread_tool.h"
#include <stdio.h>
#include <pthread.h>
void thread_sleep(size_t s)
{
fprintf(stderr, "thread#%d: sleep for %d seconds.../n",
pthread_self(), s);
struct timespec ts = {0};
ts.tv_sec = s;
nanosleep(&ts, 0);
}
我们将最终版本命名为 DPRINTF(),毕竟那是个 macro,全部大写也有助于提示使用者,这个 macro 在 release 版可能会被 preprocessor 消灭掉。实作分两部份,宣告放在 dprintf.h:
#ifndef DPRINTF_H_INCLUDED
#define DPRINTF_H_INCLUDED
#include <stddef.h>
#if defined(__cplusplus)
extern "C" {
#endif
void dprintf_c99(const char* src_file, size_t src_line,
int enable, const char* fmt, ...);
typedef void (*dprintf_fn)(int enable, const char* fmt, ...);
void dprintf_tls(int enable, const char* fmt, ...);
dprintf_fn dprintf_front(const char* src_file, size_t src_line);
#if defined(__cplusplus)
} // extern "C"
#endif
#if defined(__cplusplus)
class dprintf_cpp
{
public:
dprintf_cpp(const char* src_file, size_t src_line);
void operator()(bool enable, const char* fmt, ...) const;
private:
const char* src_file_;
size_t src_line_;
};
#endif
#ifndef NDEBUG
# if (__STDC_VERSION__ >= 199901L) // support C99
# define DPRINTF(enable, ...) /
dprintf_c99(__FILE__, __LINE__, enable, __VA_ARGS__)
# else
# if defined(__cplusplus) // is C++
# define DPRINTF /
dprintf_cpp(__FILE__, __LINE__)
# else
# define DPRINTF /
(*dprintf_front(__FILE__, __LINE__))
#endif
# endif
#else
# define DPRINTF(enable, ...) // define to nothing in release mode
#endif
#endif // DPRINTF_H_INCLUDED
实作放在 dprintf.cpp。由于使用者可能与三种实作的任何一种连结,故三种实作都必须放进产出的目的档里,不可以用 preprocessor 藏起来。因为其中一种实作使用 C++,故 dprintf.cpp 是个 C++ 原始码档案:
#include "dprintf.h"
#include "thread_tool.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <pthread.h>
static
void dprintf_impl(const char* src_file, size_t src_line,
int enable, const char* fmt, va_list ap)
{
ENTER_FUNCTION();
if (enable) {
fprintf(stderr, "%s (%d): ", src_file, src_line);
vfprintf(stderr, fmt, ap);
fprintf(stderr, "/n");
fflush(stderr);
}
}
void dprintf_c99(const char* src_file, size_t src_line,
int enable, const char* fmt, ...)
{
va_list ap;
va_start(ap, fmt);
dprintf_impl(src_file, src_line, enable, fmt, ap);
va_end(ap);
}
dprintf_cpp::dprintf_cpp(const char* src_file, size_t src_line)
: src_file_(src_file)
, src_line_(src_line)
{
}
void dprintf_cpp::operator()(bool enable, const char* fmt, ...) const
{
ENTER_FUNCTION();
va_list ap;
va_start(ap, fmt);
dprintf_impl(src_file_, src_line_, enable, fmt, ap);
va_end(ap);
}
struct file_line_t
{
const char* file;
size_t line;
};
void free_file_line(void* value)
{
ENTER_FUNCTION();
file_line_t* fl = reinterpret_cast<file_line_t*>(value);
if (fl) {
free(fl);
}
}
typedef void (*cleanup_fn)(void* value);
template <class T>
class TlsCell
{
public:
TlsCell(cleanup_fn cleanup = 0)
{
pthread_key_create(&key_, cleanup);
}
~TlsCell()
{
pthread_key_delete(key_);
}
void set(T* value)
{
pthread_setspecific(key_, reinterpret_cast<void*>(value));
}
T* get()
{
return reinterpret_cast<T*>(pthread_getspecific(key_));
}
private:
pthread_key_t key_;
};
TlsCell<file_line_t> g_file_line_tls(free_file_line);
void dprintf_tls(int enable, const char* fmt, ...)
{
ENTER_FUNCTION();
va_list ap;
file_line_t* fl = g_file_line_tls.get();
va_start(ap, fmt);
dprintf_impl(fl->file, fl->line, enable, fmt, ap);
va_end(ap);
}
dprintf_fn dprintf_front(const char* src_file, size_t src_line)
{
ENTER_FUNCTION();
file_line_t* fl = g_file_line_tls.get();
if (fl == NULL) {
fl = reinterpret_cast<file_line_t*>(malloc(sizeof(file_line_t)));
g_file_line_tls.set(fl);
}
fl->file = src_file;
fl->line = src_line;
thread_sleep(2);
return &dprintf_tls;
}
由 于 TLS 的 key 需要事先 create,事后 delete,因此将这些动作用一个叫 TlsCell 的 template class 包装起来,这样就不需要让使用者自行呼叫 pthread_key_create() 与 pthread_key_delete() 了。
为了测试 DPRINTF() 的效果,我们准备了这个测试程序标准版:
#include "dprintf.h"
#include "thread_tool.h"
#include <pthread.h>
int g_enable = 1;
int g_data = 3;
void* main_thread(void* param)
{
ENTER_FUNCTION();
thread_sleep(1);
DPRINTF(g_enable, "g_data == %d", g_data);
return 0;
}
void* other_thread(void* param)
{
ENTER_FUNCTION();
thread_sleep(2);
DPRINTF(g_enable, "g_data == %d", g_data);
return 0;
}
int main()
{
pthread_t other_thr;
pthread_create(&other_thr, NULL, other_thread, NULL);
main_thread(0);
pthread_join(other_thr, NULL);
return 0;
}
将这个标准版测试程序,内容不便,分别存成档名不同的三个档案:main_c99.c、main_cpp.cpp 与 main_tls.c。分别用不同的方法编译出 main_c99、main_cpp 与 main_tls 三支测试程序:
编译 main_c99 与执行结果:
SHELL> gcc -std=c99 -o main_c99.o -c main_c99.c
SHELL> g++ -o main_c99 main_c99.o thread_tool.o dprintf.o
SHELL> ./main_c99
thread#134557696: main_thread(): entering...
thread#134557696: sleep for 1 seconds...
thread#134558720: other_thread(): entering...
thread#134558720: sleep for 2 seconds...
thread#134557696: dprintf_impl(): entering...
main_c99.c (12): g_data == 3
thread#134558720: dprintf_impl(): entering...
main_c99.c (20): g_data == 3
编译 main_cpp 与执行结果:
SHELL> g++ -o main_cpp.o -c main_cpp.cpp
SHELL> g++ -o main_cpp main_cpp.o thread_tool.o dprintf.o
SHELL> ./main_cpp
thread#134557696: main_thread(): entering...
thread#134557696: sleep for 1 seconds...
thread#134558720: other_thread(): entering...
thread#134558720: sleep for 2 seconds...
thread#134557696: operator()(): entering...
thread#134557696: dprintf_impl(): entering...
main_cpp.cpp (12): g_data == 3
thread#134558720: operator()(): entering...
thread#134558720: dprintf_impl(): entering...
main_cpp.cpp (20): g_data == 3
编译 main_tls 与执行结果:
SHELL> gcc -o main_tls.o -c main_tls.c
SHELL> g++ -o main_tls main_tls.o thread_tool.o dprintf.o
SHELL> ./main_tls
thread#134557696: main_thread(): entering...
thread#134557696: sleep for 1 seconds...
thread#134558720: other_thread(): entering...
thread#134558720: sleep for 2 seconds...
thread#134557696: dprintf_front(): entering...
thread#134557696: sleep for 2 seconds...
thread#134558720: dprintf_front(): entering...
thread#134558720: sleep for 2 seconds...
thread#134557696: dprintf_tls(): entering...
thread#134557696: dprintf_impl(): entering...
main_tls.c (12): g_data == 3
thread#134558720: dprintf_tls(): entering...
thread#134558720: dprintf_impl(): entering...
main_tls.c (20): g_data == 3
thread#134558720: free_file_line(): entering...
从执行结果看来,这三种方法,都能够正确的运作。我们总算将 dprintf() 搞定。