天天看点

Implementing dprintf() without __VA_ARGS

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__);            /

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

#   include <pthread.h>

#endif

#if defined(WIN32)

#   define thread_id() GetCurrentThreadId()

#else

#   define thread_id() pthread_self()

#endif

#define ENTER_FUNCTION() fprintf(stderr, "thread#%d: %s(): entering.../n", thread_id(), __FUNCTION__);

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

}

#if defined(WIN32)

typedef CRITICAL_SECTION mutex_t;

#else

typedef pthread_mutex_t mutex_t;

#endif

void mutex_init(mutex_t* mx)

{

#if defined(WIN32)

    InitializeCriticalSection(mx);

#else

    pthread_mutex_init(mx, NULL);

#endif

}

void mutex_destroy(mutex_t* mx)

{

#if defined(WIN32)

    DeleteCriticalSection(mx);

#else

    pthread_mutex_destroy(mx);

#endif

}

void mutex_lock(mutex_t* mx)

{

#if defined(WIN32)

    EnterCriticalSection(mx);

#else

    pthread_mutex_lock(mx);

#endif

}

void mutex_unlock(mutex_t* mx)

{

#if defined(WIN32)

    LeaveCriticalSection(mx);

#else

    pthread_mutex_unlock(mx);

#endif

}

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

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 , 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__);            /

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__);            /

void thread_sleep(size_t s);

#if defined(__cplusplus)

} // extern "C"

#endif

#endif

与其实作 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() 搞定。