使用线程局部存储实现多线程下的日志系统(转)

http://www.ibm.com/developerworks/cn/linux/1310_qianbh_threadlog/index.html

多线程编程向来不容易,在多线程环境下实现日志系统是很多程序员亟须解决的问题。在本文中详细介绍了线程局部存储的概念、原理,并用代码示例详细展示了如何使用线程局部存储来实现多线程下的日志系统。

概述

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

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

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

背景介绍

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

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

  1. 使用 static 的线程局部变量很容易能实现线程级别的单例日志系统;
  2. 通过智能指针 boost::thread_specific_ptr 来存取线程局部存储的生成和清除工作简单方便;
  3. 使用线程局部变量很容易实现对一个已有的单例日志系统进行多线程支持的改造,并且不用改动任何原来的日志接口;

回页首

单线程环境的日志

一般来说,日志类都会实现成一个单例,从而方便调用。为简单起见,示例中的日志代码的写操作直接打印到控制台。以下是 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 的线程局部存储变量实现的一个简洁的多线程日志系统。内容包括了日志系统概述,相关的背景介绍,该日志系统的代码示例以及优势等等。该日志系统是一个线程级别的单例日志系统,具有管理简单高效,特别是对已有的不能支持多线程的单例日志系统提供了一个很好的改造思路,使用线程局部存储变量可以不用改变原来日志系统的业务接口。

参考资料

学习

时间: 2024-11-05 23:23:08

使用线程局部存储实现多线程下的日志系统(转)的相关文章

多线程下写日志

鄙人最近遇到了一个奇特的线上事故,记录一下,以备记忆. 鄙人所在的部门负责给公司提供各种基础库,即基础架构部门.最近某别的部门用本部门提供的支持多线程版本的日志库后,出现这样一个奇特的现象:当磁盘被日志写满以后,他们的数据文件的头部被写上了最新的日志!就是说,别的部门的程序的数据文件被日志数据给污染了. 这里先不介绍这个事故的原因.先说下这个日志库的写日志过程,其流程大致如下: step1  如果log的fd为-1,就重新通过C函数open再打开一个log_fd: step2  写log内容,即

多线程下的缓存系统

摘要: 1,自己设计了简易的缓存系统,以供新手理解缓存练手. 2,加强读锁.写锁的应用. 理解缓存: 实际上是内存,存取键值对,通过key可以返回value.

Linux下一个简单的日志系统的设计及其C代码实现

1.概述 在大型软件系统中,为了监测软件运行状况及排查软件故障,一般都会要求软件程序在运行的过程中产生日志文件.在日志文件中存放程序流程中的一些重要信息, 包括:变量名称及其值.消息结构定义.函数返回值及其执行情况.脚本执行及调用情况等.通过阅读日志文件,我们能够较快地跟踪程序流程,并发现程序问题. 因此,熟练掌握日志系统的编写方法并快速地阅读日志文件,是对一个软件开发工程师的基本要求. 本文详细地介绍了Linux下一个简单的日志系统的设计方法,并给出了其C代码实现.本文为相关开发项目Linux

Java日志系统框架的设计与实现

推荐一篇好的文章介绍java日志系统框架的设计的文章:http://soft.chinabyte.com/database/438/11321938.shtml 文章内容总结: 日志系统对跟踪调试.程序状态记录.数据恢复等功能有重要作用 日志系统一般作为服务进程或者系统调用存在,我们一般程序中使用系统调用 常用日志系统包括log4j的简单介绍 日志系统的系统架构 日志系统的信息分级 日志输出的设计 下面是全文的引用: 在Java领域,存在大量的日志组件,open-open收录了21个日志组件.日

多线程下C#如何保证线程安全?

多线程编程相对于单线程会出现一个特有的问题,就是线程安全的问题.所谓的线程安全,就是如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码.如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的. 线程安全问题都是由全局变量及静态变量引起的. 为了保证多线程情况下,访问静态变量的安全,可以用锁机制来保证,如下所示: 1 //需要加锁的静态全局变量 2 private static bool _isOK = false; 3 //lock只能锁定一

多线程下的进程同步(线程同步问题总结篇)

之前写过两篇关于线程同步问题的文章(一,二),这篇中将对相关话题进行总结,本文中也对.NET 4.0中新增的一些同步机制进行了介绍. 首先需要说明的是为什么需要线程功能同步.MSDN中有这样一段话很好的解释了这个问题: 当多个线程可以调用单个对象的属性和方法时,对这些调用进行同步处理是非常重要的.否则,一个线程可能会中断另一个线程正在执行的任务,使该对象处于一种无效状态. 也就说在默认无同步的情况下,任何线程都可以随时访问任何方法或字段,但一次只能有一个线程访问这些对象.另外,MSDN中也给出定

Vector 是线程安全的,是不是在多线程下操作Vector就可以不用加Synchronized

如标题一样,如果之前让我回答,我会说,是的,在多线程的环境下操作Vector,不需要加Synchronized. 但是我今天无意间看到一篇文章,我才发现我之前的想法是错误的,这篇文章的地址: http://zhangbq168.blog.163.com/blog/static/2373530520082332459511/ 我摘抄关键的一部分: Vector 比 ArrayList慢,是因为vector本身是同步的,而arraylist不是所以,没有涉及到同步的推荐用arraylist. 看jd

线程局部存储 TLS

C/C++运行库提供了TLS(线程局部存储),在多线程还未产生时,可以将数据与正在执行的线程关联.strtok()函数就是一个很好的例子.与它一起的还有strtok_s(),_tcstok_s()等等函数,其实_tcs 是 wcs 的另外一种写法,表示宽字符存储,_s 是微软定义的安全函数,通常比普通函数多一个参数.以_tcstok_s()为例, int main(int argc, char* argv[]){ wchar_t Source[] = L"192.168.255.255"

每天进步一点点——Linux中的线程局部存储(一)

转载请说明出处:http://blog.csdn.net/cywosp/article/details/26469435 在Linux系统中使用C/C++进行多线程编程时,我们遇到最多的就是对同一变量的多线程读写问题,大多情况下遇到这类问题都是通过锁机制来处理,但这对程序的性能带来了很大的影响,当然对于那些系统原生支持原子操作的数据类型来说,我们可以使用原子操作来处理,这能对程序的性能会得到一定的提高.那么对于那些系统不支持原子操作的自定义数据类型,在不使用锁的情况下如何做到线程安全呢?本文将从