歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Unix知識 >> Unix基礎知識 >> 如何使用線程局部存儲實現多線程下的日志系統

如何使用線程局部存儲實現多線程下的日志系統

日期:2017/3/3 15:20:47   编辑:Unix基礎知識

概述

通常來說,在應用程序中需要日志來記錄程序運行的狀態,以便後期問題的跟蹤定位。在日志系統的設計中,通常會有一個總的日志系統來統一協調這些日志的設置如位置、輸出級別和內容等。在多線程編程中,當每個線程都需要輸出日志時,因為要考慮線程間的同步,日志系統的設計更加復雜。

在單線程應用程序中,通常使用一個日志單例向某個文件輸出應用運行過程中的重要日志信息,但是在多線程環境中,這樣做顯然不好,因為各個線程打印出的日志會錯綜復雜從而使得日志文件不容易閱讀和跟蹤。比較好的辦法是主線程記錄自己的日志,各個子線程單獨記錄各自的日志。為了保留多線程環境中日志系統的簡單清晰特性,本文使用線程局部變量來實現多線程下的日志系統。既保證了各個線程有各自清晰的日志文件,每個線程又只有一個簡單的日志單例,從而使日志系統具有簡單清晰高效的特性並且適用單線程和多線程的應用。

本文所闡述的多線程日志系統是基於 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 的線程局部存儲變量實現的一個簡潔的多線程日志系統。內容包括了日志系統概述,相關的背景介紹,該日志系統的代碼示例以及優勢等等。該日志系統是一個線程級別的單例日志系統,具有管理簡單高效,特別是對已有的不能支持多線程的單例日志系統提供了一個很好的改造思路,使用線程局部存儲變量可以不用改變原來日志系統的業務接口。

查看本欄目更多精彩內容:http://www.bianceng.cn/OS/unix/

Copyright © Linux教程網 All Rights Reserved