如何使用线程局部存储来实现多线程下的日志系统

如何使用线程局部存储来实现多线程下的日志系统

通常来说,在应用程序中需要日志来记录程序运行的状态,以便后期问题的跟踪定位。在日志系统的设计中,通常会有一个总的日志系统来统一协调这些日志的设置如位置、输出级别和内容等。在多线程编程中,当每个线程都需要输出日志时,因为要考虑线程间的同步,日志系统的设计更加复杂。

在单线程应用程序中,通常使用一个日志单例向某个文件输出应用运行过程中的重要日志信息,但是在多线程环境中,这样做显然不好,因为各个线程打印出的日志会错综复杂从而使得日志文件不容易阅读和跟踪。比较好的办法是主线程记录自己的日志,各个子线程单独记录各自的日志。为了保留多线程环境中日志系统的简单清晰特性,本文使用线程局部变量来实现多线程下的日志系统。既保证了各个线程有各自清晰的日志文件,每个线程又只有一个简单的日志单例,从而使日志系统具有简单清晰高效的特性并且适用单线程和多线程的应用。

本文所阐述的多线程日志系统是基于 C++和 Boost 库的一个实现,对于实现多线程环境下的日志系统有很好的参考意义。如果日志系统的最初设计没有考虑多线程环境,随着业务的发展需要实现多线程,这种方法可以很方便地改造已有的日志系统,从而实现多线程日志系统,并且这种方法还能保持原来日志系统的业务接口不变。

背景介绍

对于线程局部存储的概念,正如字面意思,每个变量在每个线程中都有一份独立的拷贝。通过使用线程局部存储技术,可以避免线程间的同步问题,并且不同的线程可以使用不同的日志设置。通过 Boost 库的智能指针 boost::thread_specific_ptr 来存取线程局部存储,每个线程在第一次试图获取这个智能指针的实例时需要对它进行初始化,并且线程局部存储的数据在线程退出时由 Boost 库来释放。

使用线程局部变量的多线程日志的优势:

使用 static 的线程局部变量很容易能实现线程级别的单例日志系统;

通过智能指针 boost::thread_specific_ptr 来存取线程局部存储的生成和清除工作简单方便;

使用线程局部变量很容易实现对一个已有的单例日志系统进行多线程支持的改造,并且不用改动任何原来的日志接口;

单线程环境的日志

一般来说,日志类都会实现成一个单例,从而方便调用。为简单起见,示例中的日志代码的写操作直接打印到控制台。以下是 Logger 类的初始定义:

清单 1. Logger 类的初始定义

class Logger

{

private:

Logger() { }

public:

static void Init(const std::string &name);

static Logger *GetInstance();

void Write(const char *format, ...);

private:

static std::string ms_name;

static Logger *ms_this_logger;

};

Init()函数用来设置 Logger 的名字。在实际应用中,可以用来设置其它 Logger 的配置信息。在单线程环境中,每次调用 Write()函数就可以写日志了。

多线程环境的日志

多线程环境下实现日志系统,必须对写操作加锁,否则将得到混乱的输出。容易想到的方法是:在 Logger 类中维护一个列表,按名字存放所有线程中 Logger 的实例,并在每个线程中按名字查找并使用线程自己的唯一一个 Logger。

这跟 log4j 的实现有点像,不同的是 log4j 把所有的 Logger 配置都放在配置文件里,每个 Logger 都有独立的配置。但是我们的 Logger 无法实现这个功能,因为配置信息都是运行时传入的,并且所有的 Logger 共享的同样的配置信息。

另外一个很大的问题是,必须修改 GetInstance()的声明,加入一个类似 Logger 名字的参数。这种做法破坏了原有的 API,已有的代码必须全部修改以支持新的 Logger 类。

使用线程局部存储,可以解决以上两个问题:每个 Logger 的配置都是线程独立,并且不需要修改 GetInstance()的声明。以下是新的 Logger 类的声明,里面使用了 boost 的 thread_specific_ptr 这个类,它实现了跨平台的线程局部存储解决方案。

清单 2. 使用线程局部存储的 Logger 类定义

class Logger

{

private:

Logger() { }

public:

static void Init(const std::string &name);

static Logger *GetInstance();

void Write(const char *format, ...);

private:

static boost::thread_specific_ptr<std::string> ms_name;

static boost::thread_specific_ptr<Logger> ms_this_logger;

};

代码中简单地使用 boost::thread_specific_ptr 类重新声明了类里的两个静态变量,在运行时它们会被放到线程局部存储中。

清单 3. 使用线程局部变量的 Logger 类实现

void Logger::Init(const string &name)

{

if (!name.empty()) {

ms_name.reset(new std::string(name));

}

}

Logger *Logger::GetInstance()

{

if (ms_this_logger.get() == NULL) {

ms_this_logger.reset(new Logger);

}

return ms_this_logger.get();

}

实现代码中,调用 boost::thread_specific_ptr 类的 reset()函数来设值。下面是两个 Logger 类的简单调用代码,它创建的两个线程,在每个线程中设置 Logger 的名字:

清单 4. Logger 类的调用代码

class Thread

{

public:

Thread(const char *name) : m_name(name) { }

void operator()()

{

/* set logger name in thread */

Logger::Init(m_name);

/* call GetInstance() and Write() in other functions with thread-local enabled */

Logger *logger = Logger::GetInstance();

for (int i = 0; i < 3; i++) {

logger->Write("Hello %d", i);

#ifdef _WIN32

Sleep(1000);

#else

sleep(1);

#endif

}

}

private:

string m_name;

};

int main()

{

boost::thread t1(Thread("name1"));

boost::thread t2(Thread("name2"));

t1.join();

t2.join();

return 0;

}

对于 Logger 的初始版本,输出可能是这样:

清单 5. 初始版本 Logger 类的输出

# ./logger

[name1] Hello 0

[name2] Hello 0

[name2] Hello 1

[name2] Hello 1

[name2] Hello 2

[name2] Hello 2

第二个线程重新对 name 赋值了之后,第一个线程也收到了影响。对于使用线程局部存储的 Logger,输出如下:

清单 6. 使用线程局部存储的 Logger 类的输出

# ./logger2

[name1] Hello 0

[name2] Hello 0

[name1] Hello 1

[name2] Hello 1

[name1] Hello 2

[name2] Hello 2

两个线程中的 name 变量互相独立,分别打印出了正确的值。

boost 库的实现

boost 库是怎么实现线程局部存储的呢?通过跟踪代码,以下分别是 Windows 和 Linux 平台的调用栈(基于 1.43 版):

清单 7. boost::thread_specific_ptr 在 windows 下的调用栈

boost::thread_specific_ptr::reset()

--> boost::detail::set_tss_data()

--> boost::detail::get_or_make_current_thread_data()

--> boost::detail::get_current_thread_data()

--> ::TlsGetValue()

# reference:

# ${BOOST_SRC}/boost/thread/tss.hpp

# ${BOOST_SRC}/lib/thread/src/win32/thread.cpp

清单 8. boost::thread_specific_ptr 在 Linux 下的调用栈

boost::thread_specific_ptr::reset()

--> boost::detail::set_tss_data()

--> boost::detail::add_new_tss_node()

--> boost::detail::get_or_make_current_thread_data()

--> boost::detail::get_current_thread_data()

--> ::pthread_getspecific()

# reference:

# ${BOOST_SRC}/boost/thread/tss.hpp

# ${BOOST_SRC}/lib/thread/src/pthread/thread.cpp

在两个平台下,最后分别都调用了系统 API 来实现线程局部存储。相关的数据结构可以参考 boost 的源代码。

总结

本文通过描述了一个使用 boost::thread_specific_ptr 的线程局部存储变量实现的一个简洁的多线程日志系统。内容包括了日志系统概述,相关的背景介绍,该日志系统的代码示例以及优势等等。该日志系统是一个线程级别的单例日志系统,具有管理简单高效,特别是对已有的不能支持多线程的单例日志系统提供了一个很好的改造思路,使用线程局部存储变量可以不用改变原来日志系统的业务接口。