Skip to content

Commit 0b5fde6

Browse files
committed
修改第四章,增加一节内容,“线程存储期”
1 parent 7e9725e commit 0b5fde6

File tree

1 file changed

+58
-1
lines changed

1 file changed

+58
-1
lines changed

md/03共享数据.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -894,8 +894,65 @@ void* operator new (std::size_t count){
894894
895895
C++ 只保证了 `operator new``operator delete` 这两个方面的线程安全(不包括用户定义的),其它方面就得自己保证了。前面的内容也都提到了。
896896
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+
897954
## 总结
898955
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` 运算符的库函数实际是线程安全的,以及线程存储期
900957
901958
下一章,我们将开始讲述同步操作,会使用到 [**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

Comments
 (0)