@@ -894,8 +894,65 @@ void* operator new (std::size_t count){
894
894
895
895
C++ 只保证了 ` operator new ` 、` operator delete ` 这两个方面的线程安全(不包括用户定义的),其它方面就得自己保证了。前面的内容也都提到了。
896
896
897
+ ## 线程存储期
898
+
899
+ ** 线程存储期** (也有人喜欢称作“[ * 线程局部存储* ] ( https://zh.wikipedia.org/wiki/%E7%BA%BF%E7%A8%8B%E5%B1%80%E9%83%A8%E5%AD%98%E5%82%A8 ) ”)的概念源自操作系统,是一种非常古老的机制,广泛应用于各种编程语言。线程存储期的对象在线程开始时分配,并在线程结束时释放。每个线程拥有自己独立的对象实例,互不干扰。在 C++11中,引入了[ ** ` thread_local ` ** ] ( https://zh.cppreference.com/w/cpp/keyword/thread_local ) 关键字,用于声明具有线程存储期的对象。
900
+
901
+ 以下是一个示例代码,展示了 ` thread_local ` 关键字的使用:
902
+
903
+ ``` cpp
904
+ int global_counter = 0 ;
905
+ thread_local int thread_local_counter = 0 ;
906
+
907
+ void print_counters (){
908
+ std::cout << "global:" << global_counter++ << '\n';
909
+ std::cout << "thread_local:" << thread_local_counter++ << '\n';
910
+ }
911
+
912
+ int main (){
913
+ std::thread{ print_counters }.join();
914
+ std::thread{ print_counters }.join();
915
+ }
916
+ ```
917
+
918
+ [ ** 运行结果** ] ( https://godbolt.org/z/ncxTEce7f ) :
919
+
920
+ ``` txt
921
+ global:0
922
+ thread_local:0
923
+ global:1
924
+ thread_local:0
925
+ ```
926
+
927
+ 这段代码很好的展示了 ` thread_local ` 关键字的使用以及它的作用。每一个线程都有独立的 ` thread_local_counter ` 对象,它们不是同一个。
928
+
929
+ ---
930
+
931
+ 我知道你会有问题:“那么 C++11 之前呢?”那时开发者通常使用 POSIX 线程(Pthreads)或 Win32 线程的接口,或者依赖各家编译器的扩展。例如:
932
+
933
+ - [ ** POSIX** ] ( https://pubs.opengroup.org/onlinepubs/9699919799/ ) :使用 ` pthread_key_t ` 和相关的函数( ` pthread_key_create ` 、` pthread_setspecific ` 、` pthread_getspecific ` 和` pthread_key_delete ` )来管理线程局部存储。
934
+ - ** Win32** :使用 [ TLS(Thread Local Storage)] ( https://learn.microsoft.com/zh-cn/windows/win32/procthread/using-thread-local-storage ) 机制,通过函数 ` TlsAlloc ` 、` TlsSetValue ` 、` TlsGetValue ` 和 ` TlsFree ` 来实现线程局部存储。
935
+ - ** GCC** :使用 [ ` __thread ` ] ( https://gcc.gnu.org/onlinedocs/gcc/extensions-to-the-c-language-family/thread-local-storage.html ) 。
936
+ - ** MSVC** :使用 [ ` __declspec(thread) ` ] ( https://learn.microsoft.com/zh-cn/cpp/cpp/thread?view=msvc-170 ) 。
937
+
938
+ POSIX 与 Win32 接口的就不再介绍了,有兴趣参见我们的链接即可。我们就拿先前的代码改成使用 GCC 与 MSVC 的编译器扩展即可。
939
+
940
+ ``` cpp
941
+ __thread int thread_local_counter = 0 ; // GCC
942
+ __declspec (thread) int thread_local_counter = 0; // MSVC
943
+ ```
944
+
945
+ MSVC 无法使用 GCC 的编译器扩展,GCC 也肯定无法使用 MSVC 的扩展,不过 Clang 编译器可以,它支持 `__thread` 与 `__declspec(thread)` 两种。Clang 默认情况就支持 GCC 的编译器扩展,如果要支持 MSVC,需要设置 [`-fms-extensions`](https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-fms-extensions) 编译选项。
946
+
947
+ - [**`__declspec(thread)` 运行测试**](https://godbolt.org/z/eMz8Yz3hv)
948
+ - [**`__thread` 运行测试**](https://godbolt.org/z/Wx6zKhK44)
949
+
950
+ 要注意的是,这些扩展并不是标准的 C++ 语言特性,它们的跨平台性和可移植性较差,我们应当使用 C++ 标准的 `thread_local`。
951
+
952
+ 了解其它 API 以及编译器扩展有助于理解历史上线程存储期的演进。同时扩展知识面。
953
+
897
954
## 总结
898
955
899
- 本章讨论了多线程的共享数据引发的恶性条件竞争会带来的问题。并说明了可以使用互斥量(` std::mutex ` )保护共享数据,并且要注意互斥量上锁的“** 粒度** ”。C++标准库提供了很多工具,包括管理互斥量的管理类(` std::lock_guard ` ),但是互斥量只能解决它能解决的问题,并且它有自己的问题(** 死锁** )。同时我们讲述了一些避免死锁的方法和技术。还讲了一下互斥量所有权转移。然后讨论了面对不同情况保护共享数据的不同方式,使用 ` std::call_once() ` 保护共享数据的初始化过程,使用读写锁(` std::shared_mutex ` )保护不常更新的数据结构。以及特殊情况可能用到的互斥量 ` recursive_mutex ` ,有些人可能喜欢称作:** 递归锁** 。最后聊了一下 ` new ` 、` delete ` 运算符的库函数实际是线程安全的,以及一些问题 。
956
+ 本章讨论了多线程的共享数据引发的恶性条件竞争会带来的问题。并说明了可以使用互斥量(`std::mutex`)保护共享数据,并且要注意互斥量上锁的“**粒度**”。C++标准库提供了很多工具,包括管理互斥量的管理类(`std::lock_guard`),但是互斥量只能解决它能解决的问题,并且它有自己的问题(**死锁**)。同时我们讲述了一些避免死锁的方法和技术。还讲了一下互斥量所有权转移。然后讨论了面对不同情况保护共享数据的不同方式,使用 `std::call_once()` 保护共享数据的初始化过程,使用读写锁(`std::shared_mutex`)保护不常更新的数据结构。以及特殊情况可能用到的互斥量 `recursive_mutex`,有些人可能喜欢称作:**递归锁**。最后聊了一下 `new`、`delete` 运算符的库函数实际是线程安全的,以及线程存储期 。
900
957
901
958
下一章,我们将开始讲述同步操作,会使用到 [**Futures**](https://zh.cppreference.com/w/cpp/thread#.E6.9C.AA.E6.9D.A5.E4.BD.93)、[**条件变量**](https://zh.cppreference.com/w/cpp/thread#.E6.9D.A1.E4.BB.B6.E5.8F.98.E9.87.8F)等设施。
0 commit comments