C++之thread_local變量的一些用法
1.C++ 的存儲類型
1.1.存儲周期(Storage duration)
存儲周期表示一個變量的存儲空間持續(xù)的時間,它應該與對象的語義生命周期一致(或至少不小于對象的語義生命周期)。C++ 98從 C 繼承了三種存儲周期,分別是靜態(tài)存儲周期(static storage duration)、自動存儲周期(automatic storage duration)和動態(tài)存儲周期(dynamic storage duration),C++ 11 又增加了一種線程存儲周期(thread storage duration)。
存儲周期只是一個概念,是程序語義范疇內(nèi)的東西,但不是語法的范疇。這個概念在語法上的表示則由下一節(jié)介紹的存儲類型說明符(Storage class specifiers)展示。
1.2.存儲類型說明符(Storage class specifiers)
存儲類型說明符(Storage class specifiers)也被稱為存儲類型,它們是變量聲明語法中類型說明符的一部分,它們和變量名的范圍一起控制變量的兩個獨立屬性,即存儲周期(storage duration)和鏈接屬性( linkage)。C++ 98從 C 語言繼承了 auto、register、static 和 extern 四種類型,同時補充了一種 mutable,C++ 11 針對線程存儲周期又增加了一個線程本地存儲的說明符 thread_local。關(guān)于這幾個存儲類型說明符的作用,請參考下表:
類型 | 說明 | 備注 |
---|---|---|
auto | 自動存儲周期,也是變量的默認存儲類型,由變量的域范圍決定變量的存儲周期,比如局部變量的存儲周期隨著域的結(jié)束而結(jié)束,而全局變量的存儲周期則與程序的運行時間一致 | 從 C++11 開始,顯示使用 auto 存儲類型會導致編譯錯誤。比如 auto int i; 會導致編譯錯誤 |
register | 也是自動存儲類型,不過暗示編譯器會擇機將其放置在寄存器中以提高數(shù)據(jù)存取的效率 | 在 C++ 17 被移除標準,以后應避免使用這個存儲類型 |
static | 靜態(tài)或線程存儲周期,采用內(nèi)部鏈接(對于不屬于匿名名字空間(anonymous namespace)的靜態(tài)類成員,采用外部鏈接) | static 表示一個對象具有靜態(tài)存儲持續(xù)周期。它的生命周期是程序的整個執(zhí)行過程,其存儲的值在程序啟動之前只初始化一次 |
extern | 靜態(tài)或線程存儲周期,采用外部鏈接 | |
mutable | 嚴格來說,這不是一種存儲類型,因為它既不影響變量的存儲周期,也不影響鏈接屬性,它只是表示一種可以“不動聲色”地修改常量對象成員的機會。 | |
thread_local | 線程存儲類型 |
1.3.存儲類型說明符與存儲周期的關(guān)系
C++ 中變量存儲周期與變量類型說明符的關(guān)系如下表所示:
存儲周期 | 變量類型與類型說明符 |
---|---|
自動存儲周期 | 顯式使用 register 聲明的變量,或隱式聲明為 static 或 extern 的作用域內(nèi)部變量,沒有明確指定存儲類型說明符的變量 |
靜態(tài)存儲周期 | 1、非 thread_local 聲明的全局(非局部)變量;2、非動態(tài)生成(使用 new 創(chuàng)建)的非局部變量;3、用 static 聲明的局部變量、全局變量和類成員變量 |
動態(tài)存儲周期 | 1、使用 new 表達式創(chuàng)建(非 placement_new),并且使用 delete 銷毀的對象;2、使用其他動態(tài)分配函數(shù)和動態(tài)釋放函數(shù)管理的對象存儲位置 |
線程存儲周期 | 使用 thread_local 聲明的所有變量,包括局部變量、全局變量和成員變量 |
2.thread_local簡介
thread_local 是 C++11 為線程安全引進的變量聲明符。表示對象的生命周期屬于線程存儲期。
線程局部存儲(Thread Local Storage,TLS)是一種存儲期(storage duration),對象的存儲是在線程開始時分配,線程結(jié)束時回收,每個線程有該對象自己的實例;如果類的成員函數(shù)內(nèi)定義了 thread_local 變量,則對于同一個線程內(nèi)的該類的多個對象都會共享一個變量實例,并且只會在第一次執(zhí)行這個成員函數(shù)時初始化這個變量實例。
thread_local 一般用于需要保證線程安全的函數(shù)中。本質(zhì)上,就是線程域的全局靜態(tài)變量。
3.thread_local 應用
3.1.thread_local 與全局變量
使用 thread_local 聲明的變量會在每個線程中維護一個該變量的實例,線程之間互不影響,這里我們用一個普通的全局變量和一個 thread_local 類型的全局變量做對比,說明一下這種存儲類型的變量有什么性質(zhì)。
std::mutex print_mtx; //避免打印被沖斷 thread_local int thread_count = 1; int global_count = 1; void ThreadFunction(const std::string& name, int cpinc) { for (int i = 0; i < 5; i++) { std::this_thread::sleep_for(std::chrono::seconds(1)); std::lock_guard<std::mutex> lock(print_mtx); std::cout << "thread name: " << name << ", thread_count = " << thread_count << ", global_count = " << global_count++ << std::endl; thread_count += cpinc; } } int main() { std::thread t1(ThreadFunction, "t1", 2); std::thread t2(ThreadFunction, "t2", 5); t1.join(); t2.join(); }
輸出:
thread name: t2, thread_count = 1, global_count = 1
thread name: t1, thread_count = 1, global_count = 2
thread name: t1, thread_count = 3, global_count = 3
thread name: t2, thread_count = 6, global_count = 4
thread name: t1, thread_count = 5, global_count = 5
thread name: t2, thread_count = 11, global_count = 6
thread name: t1, thread_count = 7, global_count = 7
thread name: t2, thread_count = 16, global_count = 8
thread name: t1, thread_count = 9, global_count = 9
thread name: t2, thread_count = 21, global_count = 10
可以看出來每個線程中的 thread_count 都是從 1 開始打印,這印證了 thread_local 存儲類型的變量會在線程開始時被初始化,每個線程都初始化自己的那份實例。另外,兩個線程的打印數(shù)據(jù)也印證了 thread_count 的值在兩個線程中互相不影響。作為對比的 global_count 是靜態(tài)存儲周期,就沒有這個特性,兩個線程互相產(chǎn)生了影響。
3.2.thread_local 與 static變量
thread_local 也可以用于局部變量的聲明,其作用域的約束與局部靜態(tài)變量類似,但是其存儲與局部靜態(tài)變量不一樣,首先是每個線程都有自己的變量實例,其次是其生命周期與線程一致,而局部靜態(tài)變量的聲明周期是直到程序結(jié)束。下面再用一個例子演示一下:
void DoPrint(const std::string& name, int cpinc) { static int static_count = 1; thread_local int local_count = 1; std::cout << "thread name: " << name << ", local_count = " << local_count << ", static_count = " << static_count++ << std::endl; local_count += cpinc; } void ThreadFunction(const std::string& name, int cpinc) { for (int i = 0; i < 5; i++) { std::this_thread::sleep_for(std::chrono::seconds(1)); std::lock_guard<std::mutex> lock(print_mtx); DoPrint(name, cpinc); } } int main() { std::thread t1(ThreadFunction, "t1", 2); std::thread t2(ThreadFunction, "t2", 5); t1.join(); t2.join(); }
在上面的例子中,static_count 和 local_count 變量的作用域都僅限于 DoPrint() 函數(shù)內(nèi)部,但是存儲類型不一樣,local_count 在每個線程中的實例獨立初始化,獨立變化,線程之間沒有影響,而局部靜態(tài)變量 static_count 則在兩個線程之間互相影響。從結(jié)果打印的情況也印證了這一點:
thread name: t1, local_count = 1, static_count = 1
thread name: t2, local_count = 1, static_count = 2
thread name: t1, local_count = 3, static_count = 3
thread name: t2, local_count = 6, static_count = 4
thread name: t2, local_count = 11, static_count = 5
thread name: t1, local_count = 5, static_count = 6
thread name: t1, local_count = 7, static_count = 7
thread name: t2, local_count = 16, static_count = 8
thread name: t1, local_count = 9, static_count = 9
thread name: t2, local_count = 21, static_count = 10
3.3.thread_local 與 成員變量
thread_local 可以用于類的成員變量,但是只能用于靜態(tài)成員變量。這很容易理解,C++ 不能在對象只有一份拷貝的情況下弄出多個成員變量的實例,但是靜態(tài)成員就不一樣了,每個類的靜態(tài)成員共享一個實例,改成線程局部存儲比較容易實現(xiàn),也容易理解。
class B { public: B() { std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "create B" << std::endl; } ~B() {} thread_local static int b_key; //thread_local int b_key; int b_value = 24; static int b_static; }; thread_local int B::b_key = 12; int B::b_static = 36; void thread_func(const std::string& thread_name) { B b; for (int i = 0; i < 3; ++i) { b.b_key--; b.b_value--; b.b_static--; // not thread safe std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "thread[" << thread_name << "]: b_key:" << b.b_key << ", b_value:" << b.b_value << ", b_static:" << b.b_static << std::endl; std::cout << "thread[" << thread_name << "]: B::key:" << B::b_key << ", b_value:" << b.b_value << ", b_static: " << B::b_static << std::endl; return; }
輸出:
create B
thread[t2]: b_key:11, b_value:23, b_static:35
thread[t2]: B::key:11, b_value:23, b_static: 35
thread[t2]: b_key:10, b_value:22, b_static:34
thread[t2]: B::key:10, b_value:22, b_static: 34
thread[t2]: b_key:9, b_value:21, b_static:33
thread[t2]: B::key:9, b_value:21, b_static: 33
create B
thread[t1]: b_key:11, b_value:23, b_static:32
thread[t1]: B::key:11, b_value:23, b_static: 32
thread[t1]: b_key:10, b_value:22, b_static:31
thread[t1]: B::key:10, b_value:22, b_static: 31
thread[t1]: b_key:9, b_value:21, b_static:30
thread[t1]: B::key:9, b_value:21, b_static: 30
b_key
是 thread_local,雖然其也是 static 的,但是每個線程中有一個,每次線程中的所有調(diào)用共享這個變量。b_static
是真正的 static,全局只有一個,所有線程共享這個變量。
3.4.thread_local 與初始化
#include <iostream> #include <thread> #include <mutex> std::mutex cout_mutex; //定義類 class A { public: A() { std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "create A" << std::endl; } ~A() { std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "destroy A" << std::endl; } int counter = 0; int get_value() { return counter++; } }; A* creatA() { return new A(); } void loopin_func(const std::string& thread_name) { thread_local A* a = creatA(); std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "thread[" << thread_name << "]: a.counter:" << a->get_value() << std::endl; return; } void thread_func(const std::string& thread_name) { for (int i = 0; i < 3; ++i) { loopin_func(thread_name); } return; } int main() { std::thread t1(thread_func, "t1"); std::thread t2(thread_func, "t2"); t1.join(); t2.join(); return 0; }
輸出:
create A
thread[t1]: a.counter:0
thread[t1]: a.counter:1
thread[t1]: a.counter:2
create A
thread[t2]: a.counter:0
thread[t2]: a.counter:1
thread[t2]: a.counter:2
雖然 createA()
看上去被調(diào)用了多次,實際上只被調(diào)用了一次,因為 thread_local 變量只會在每個線程最開始被調(diào)用的時候進行初始化,并且只會被初始化一次。
舉一反三,如果不是初始化,而是賦值,則情況就不同了:
void loopin_func(const std::string& thread_name) { thread_local A* a; a = creatA(); std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "thread[" << thread_name << "]: a.counter:" << a->get_value() << std::endl; return; }
輸出:
create A
thread[t1]: a.counter:0
thread[t1]: a.counter:0
thread[t1]: a.counter:0
create A
thread[t2]: a.counter:0
thread[t2]: a.counter:0
thread[t2]: a.counter:0
很明顯,雖然只初始化一次,但卻可以被多次賦值,因此 C++ 變量初始化是十分重要的。
4.thread_local 的用處
在 thread_local 提出之前,你無法為一個線程定義自己的全局變量(線程級別的全局變量),只能將全局變量定義在父進程中,由所有的線程(不同種類的線程)共享使用。但是當程序復雜到一定程度的時候,線程之間的串擾就在所難免,同時也增大了多線程編碼的復雜度。前面的例子展示了 thread_local 的用法,每個線程共享一個屬于本線程的變量的實例,相當于線程有了自己的全局變量。
另一個常用來解釋 thread_local 的意義的例子就是隨機數(shù)的生成。我們知道的隨機數(shù)生成器都是偽隨機數(shù)生成器,其隨機性取決于種子(seed)的變化。如果一個函數(shù)使用局部變量設置隨機數(shù)發(fā)生器的種子,那么它在每個使用這個函數(shù)的線程中都會被初始化,由于使用了相同的種子,每個線程將得到一樣的隨機數(shù)序列,這就使得多線程也不那么隨機了。如果使用 thread_local 類型的種子,則每個線程維護自己的種子,從而使得每個線程都能得到不同的隨機數(shù)序列,真正起到隨機數(shù)的作用。
其他的例子就是線程不安全問題,C 標準庫的錯誤碼 errno,還有 strtok() 等函數(shù)就是線程不安全的例子。有了 thread_local ,就可以用很小的改動解決這些函數(shù)的線程不安全問題。也不需要像有些編譯器那樣,專門提供一套線程安全的標準庫,用過的人都知道,很多函數(shù)的參數(shù)定義都是不兼容的,對現(xiàn)有代碼的改造成本非常高。
5.性能考慮
- 雖然
thread_local
變量提供了線程間的數(shù)據(jù)隔離,但它們也可能帶來一些性能開銷。 - 訪問
thread_local
變量通常比訪問常規(guī)的全局變量或棧變量要慢,因為需要進行額外的 TLS 查找操作。 - 因此,在性能敏感的代碼中應謹慎使用
thread_local
。
6.替代方案
- 如果不需要真正的線程局部存儲,但只是想在線程之間傳遞數(shù)據(jù),可以考慮使用線程特定的數(shù)據(jù)(Thread-Specific Data, TSD)機制,如 POSIX 的
pthread_key_create
和pthread_setspecific
函數(shù)。 - 對于跨平臺的應用程序,可以使用第三方庫(如 Boost.Thread)來提供類似的功能。
7.總結(jié)
thread_local
關(guān)鍵字為 C++ 程序員提供了一種方便的方式來處理多線程環(huán)境中的線程特定數(shù)據(jù)。通過避免數(shù)據(jù)競爭和簡化同步機制,它可以幫助提高多線程程序的性能和可維護性。然而,在使用時需要注意其性能開銷和跨平臺兼容性問題,并根據(jù)具體場景選擇合適的替代方案。
到此這篇關(guān)于C++之thread_local變量的文章就介紹到這了,更多相關(guān)C++ thread_local變量內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解C語言用malloc函數(shù)申請二維動態(tài)數(shù)組的實例
這篇文章主要介紹了詳解C語言用malloc函數(shù)申請二維動態(tài)數(shù)組的實例的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-10-10