c++ 線程局部變量thread_local

Linux中的線程局部存儲(一)

  本章節轉自:https://blog.csdn.net/cywosp/article/details/26469435

  在Linux系統中使用C/C++進行多線程編程時,我們遇到最多的就是對同一變量的多線程讀寫問題,大多情況下遇到這類問題都是通過鎖機制來處理,但這對程序的性能帶來了很大的影響,當然對於那些系統原生支持原子操作的數據類型來說,我們可以使用原子操作來處理,這能對程序的性能會得到一定的提高。

  那麼對於那些系統不支持原子操作的自定義數據類型,在不使用鎖的情況下如何做到線程安全呢?本文將從線程局部存儲方面,簡單講解處理這一類線程安全問題的方法。

一、數據類型

  在C/C++程序中常存在全局變量、函數內定義的靜態變量以及局部變量,對於局部變量來說,其不存在線程安全問題,因此不在本文討論的範圍之內。全局變量和函數內定義的靜態變量,是同一進程中各個線程都可以訪問的共享變量,因此它們存在多線程讀寫問題。

  在一個線程中修改了變量中的內容,其他線程都能感知並且能讀取已更改過的內容,這對數據交換來說是非常快捷的,但是由於多線程的存在,對於同一個變量可能存在兩個或兩個以上的線程同時修改變量所在的內存內容,同時又存在多個線程在變量在修改的時去讀取該內存值,如果沒有使用相應的同步機制來保護該內存的話,那麼所讀取到的數據將是不可預知的,甚至可能導致程序崩潰。

  如果需要在一個線程內部的各個函數調用都能訪問、但其它線程不能訪問的變量,這就需要新的機制來實現,我們稱之爲Static memory local to a thread (線程局部靜態變量),同時也可稱之爲線程特有數據(TSD: Thread-Specific Data)或者線程局部存儲(TLS: Thread-Local Storage)。這一類型的數據,在程序中每個線程都會分別維護一份變量的副本(copy),並且長期存在於該線程中,對此類變量的操作不影響其他線程。如下圖:

二、一次性初始化

  在講解線程特有數據之前,先讓我們來了解一下一次性初始化。多線程程序有時有這樣的需求:不管創建多少個線程,有些數據的初始化只能發生一次。列如:在C++程序中某個類在整個進程的生命週期內只能存在一個實例對象,在多線程的情況下,爲了能讓該對象能夠安全的初始化,一次性初始化機制就顯得尤爲重要了。——在設計模式中這種實現常常被稱之爲單例模式(Singleton)。

  Linux中提供瞭如下函數來實現一次性初始化:

#include <pthread.h>

// Returns 0 on success, or a positive error number on error

int pthread_once (pthread_once_t *once_control, void (*init) (void));

  利用參數once_control的狀態,函數pthread_once()可以確保無論有多少個線程調用多少次該函數,也只會執行一次由init所指向的由調用者定義的函數。init所指向的函數沒有任何參數,形式如下:

void init (void)

{

   // some variables initializtion in here

}

  另外,參數once_control必須是pthread_once_t類型變量的指針,指向初始化爲PTHRAD_ONCE_INIT的靜態變量。在C++0x以後提供了類似功能的函數std::call_once (),用法與該函數類似。使用實例請參考https://github.com/ApusApp/Swift/blob/master/swift/base/singleton.hpp實現。

三、線程局部數據API

  在Linux中提供瞭如下函數來對線程局部數據進行操作

#include <pthread.h>

// Returns 0 on success, or a positive error number on error

int pthread_key_create (pthread_key_t *key, void (*destructor)(void *));

 

// Returns 0 on success, or a positive error number on error

int pthread_key_delete (pthread_key_t key);

 

// Returns 0 on success, or a positive error number on error

int pthread_setspecific (pthread_key_t key, const void *value);

 

// Returns pointer, or NULL if no thread-specific data is associated with key

void *pthread_getspecific (pthread_key_t key);

  函數pthread_key_create()爲線程局部數據創建一個新鍵,並通過key指向新創建的鍵緩衝區。因爲所有線程都可以使用返回的新鍵,所以參數key可以是一個全局變量(在C++多線程編程中一般不使用全局變量,而是使用單獨的類對線程局部數據進行封裝,每個變量使用一個獨立的pthread_key_t)。destructor所指向的是一個自定義的函數,其格式如下:

void Dest (void *value)

{

    // Release storage pointed to by 'value'

}

  只要線程終止時與key關聯的值不爲NULL,則destructor所指的函數將會自動被調用。如果一個線程中有多個線程局部存儲變量,那麼對各個變量所對應的destructor函數的調用順序是不確定的,因此,每個變量的destructor函數的設計應該相互獨立。

  函數pthread_key_delete()並不檢查當前是否有線程正在使用該線程局部數據變量,也不會調用清理函數destructor,而只是將其釋放以供下一次調用pthread_key_create()使用。在Linux線程中,它還會將與之相關的線程數據項設置爲NULL。

  由於系統對每個進程中pthread_key_t類型的個數是有限制的,所以進程中並不能創建無限個的pthread_key_t變量。Linux中可以通過PTHREAD_KEY_MAX(定義於limits.h文件中)或者系統調用sysconf(_SC_THREAD_KEYS_MAX)來確定當前系統最多支持多少個鍵。Linux中默認是1024個鍵,這對於大多數程序來說已經足夠了。如果一個線程中有多個線程局部存儲變量,通常可以將這些變量封裝到一個數據結構中,然後使封裝後的數據結構與一個線程局部變量相關聯,這樣就能減少對鍵值的使用。

  函數pthread_setspecific()用於將value的副本存儲於一數據結構中,並將其與調用線程以及key相關聯。參數value通常指向由調用者分配的一塊內存,當線程終止時,會將該指針作爲參數傳遞給與key相關聯的destructor函數。當線程被創建時,會將所有的線程局部存儲變量初始化爲NULL,因此第一次使用此類變量前必須先調用pthread_getspecific()函數來確認是否已經於對應的key相關聯,如果沒有,那麼pthread_getspecific()會分配一塊內存並通過pthread_setspecific()函數保存指向該內存塊的指針。

  參數value的值也可以不是一個指向調用者分配的內存區域,而是任何可以強制轉換爲void*的變量值,在這種情況下,先前的pthread_key_create()函數應將參數

  destructor設置爲NULL

  函數pthread_getspecific()正好與pthread_setspecific()相反,其是將pthread_setspecific()設置的value取出。在使用取出的值前最好是將void*轉換成原始數據類型的指針。

四、深入理解線程局部存儲機制

  1. 深入理解線程局部存儲的實現有助於對其API的使用。在典型的實現中包含以下數組:

  一個全局(進程級別)的數組,用於存放線程局部存儲的鍵值信息。pthread_key_create()返回的pthread_key_t類型值只是對全局數組的索引,該全局數組標記爲pthread_keys,其格式大概如下:

  數組的每個元素都是一個包含兩個字段的結構,第一個字段標記該數組元素是否在用,第二個字段用於存放針對此鍵、線程局部存儲變的解構函數的一個副本,即destructor函數。

  每個線程還包含一個數組,存有爲每個線程分配的線程特有數據塊的指針(通過調用pthread_setspecific()函數來存儲的指針,即參數中的value)

  2. 在常見的存儲pthread_setspecific()函數參數value的實現中,大多數都類似於下圖的實現。圖中假設pthread_keys[1]分配給func1()函數,pthread API爲每個函數維護指向線程局部存儲數據塊的一個指針數組,其中每個數組元素都與圖線程局部數據鍵的實現(上圖)中的全局pthread_keys中元素一一對應。

五、小結

  使用全局變量或者靜態變量是導致多線程編程中非線程安全的常見原因。在多線程程序中,保障非線程安全的常用手段之一是使用互斥鎖來做保護,這種方法帶來了併發性能下降,同時也只能有一個線程對數據進行讀寫。如果程序中能避免使用全局變量或靜態變量,那麼這些程序就是線程安全的,性能也可以得到很大的提升。如果有些數據只能有一個線程可以訪問,那麼這一類數據就可以使用線程局部存儲機制來處理,雖然使用這種機制會給程序執行效率上帶來一定的影響,但對於使用鎖機制來說,這些性能影響將可以忽略。

  Linux C++的線程局部存儲簡單實現可參考https://github.com/ApusApp/Swift/blob/master/swift/base/threadlocal.h,更詳細且高效的實現可參考Facebook的folly庫中的ThreadLocal實現。更高性能的線程局部存儲機制就是使用__thread,這將在下一節中討論。

Linux中的線程局部存儲(二)

  本章節轉自:https://blog.csdn.net/cywosp/article/details/26876231

  在Linux中還有一種更爲高效的線程局部存儲方法,就是使用關鍵字__thread來定義變量。__thread是GCC內置的線程局部存儲設施(Thread-Local Storage),它的實現非常高效,與pthread_key_t向比較更爲快速,其存儲性能可以與全局變量相媲美,而且使用方式也更爲簡單。創建線程局部變量只需簡單的在全局或者靜態變量的聲明中加入__thread說明即可。列如:

    static __thread char t_buf[32] = {'\0'};

    extern __thread int t_val = 0;

  凡是帶有__thread的變量,每個線程都擁有該變量的一份拷貝,且互不干擾。線程局部存儲中的變量將一直存在,直至線程終止,當線程終止時會自動釋放這一存儲。__thread並不是所有數據類型都可以使用的,因爲其只支持POD(Plain old data structure)[1]類型,不支持class類型——其不能自動調用構造函數和析構函數。同時__thread可以用於修飾全局變量、函數內的靜態變量,但是不能用於修飾函數的局部變量或者class的普通成員變量。另外,__thread變量的初始化只能用編譯期常量,例如:

    __thread std::string t_object_1 ("Swift");                   // 錯誤,因爲不能調用對象的構造函數

    __thread std::string* t_object_2 = new std::string (); // 錯誤,初始化必須用編譯期常量

    __thread std::string* t_object_3 = nullptr;                // 正確,但是需要手工初始化並銷燬對象

  除了以上之外,關於線程局部存儲變量的聲明和使用還需注意一下幾點:

  如果變量聲明中使用量關鍵字static或者extern,那麼關鍵字__thread必須緊隨其後。

  與一般的全局變量或靜態變量一樣,線程局部變量在聲明時可以設置一個初始化值。

  可以使用C語言取地址符(&)來獲取線程局部變量的地址。

  __thread的使用例子可參考https://github.com/ApusApp/Swift/blob/master/swift/base/logging.cpp的實現及其單元測試對於那些非POD數據類型,如果想使用線程局部存儲機制,可以使用對pthread_key_t封裝的類來處理,具體方式可參考https://github.com/ApusApp/Swift/blob/master/swift/base/threadlocal.h的實現以及其的單元測試。

c++ 線程局部變量thread_local

  本章節轉自:https://blog.csdn.net/d_guco/article/details/86562943

  c++11 中添加了新的關鍵字thread_local,用來聲明新的存儲期(線程存儲期變量),即線程局部變量。

  存儲類指定符是名稱聲明語法的 decl-specifier-seq 的一部分。與名稱的作用域一同,它們控制名稱的二個獨立屬性:其“存儲期”與其“鏈接”。auto - 自動存儲期(C++11 起)。register - 自動存儲期。亦提示編譯器將此對象置於處理器的寄存器。C++17 前(棄用),static - 靜態或線程存儲期和內部鏈接。extern - 靜態或線程存儲期和外部鏈接,thread_local - 線程存儲期。

存儲期

  程序中的所有對象擁有下列存儲期之一:

  1 自動存儲期。對象的存儲在外圍代碼塊開始時分配,而在結束時解分配。除了聲明爲 static 、 extern 或 thread_local 的所有局部對象擁有此存儲期。

  2 靜態存儲期。對象的存儲在程序開始時分配,而在程序結束時解分配。只存在對象的一個實例。所有聲明於命名空間作用域(包含全局命名空間)的對象,加上聲明帶有 static 或 extern 的對象擁有此存儲期。

  3 線程存儲期。對象的存儲在線程開始時分配,而在線程結束時解分配。每個線程擁有其自身的對象實例。唯有聲明爲 thread_local 的對象擁有此存儲期。 thread_local 能與 static 或 extern 一同出現,以調整鏈接。

  4 (C++11 起)動態存儲期。通過使用動態內存分配函數,由請求分配和解分配對象的存儲。

什麼是thread_local

  關於thread_local。thread_specific_ptr代表了一個全局的變量,而在每個線程中都各自new一個線程本地的對象交給它進行管理,這樣,各個線程就可以各自獨立地訪問這個全局變量的本地存儲版本,線程之間就不會因爲訪問同一全局對象而引起資源競爭導致性能下降。而線程結束時,這個資源會被自動釋放。

  C++11的thread_local來自於boost thread_specific_ptr。

#include <iostream>

#include <string>

#include <thread>

#include <mutex>

 

thread_local unsigned int rage = 1;

std::mutex cout_mutex;

 

void increase_rage(const std::string& thread_name)

{

    ++rage; // 在鎖外修改 OK ;這是線程局域變量

    std::lock_guard<std::mutex> lock(cout_mutex);

    std::cout << "Rage counter for " << thread_name << ": " << rage << '\n';

}

 

int main()

{

    std::thread a(increase_rage, "a"), b(increase_rage, "b");

    {

        std::lock_guard<std::mutex> lock(cout_mutex);

        std::cout << "Rage counter for main: " << rage << '\n';

    }

    a.join();

    b.join();

}

/*

Rage counter for a: 2

Rage counter for main: 1

Rage counter for b: 2

*/

  我們可以看到,聲明爲線程存儲期的變量,每個線程之間互不影響,通俗的講我們可以認爲每個線程擁有此變量的一個副本,每個線程之前互不影響,其底層的原理Linux中的線程局部存儲等提供的api接口。

發佈了16 篇原創文章 · 獲贊 11 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章