File tree 1 file changed +37
-0
lines changed
1 file changed +37
-0
lines changed Original file line number Diff line number Diff line change @@ -1005,6 +1005,43 @@ CPU 变量的概念很好理解。就像线程变量为每个线程提供独立
1005
1005
1006
1006
[^3]: [Linux 内核版本历史](https://zh.wikipedia.org/wiki/Linux%E5%86%85%E6%A0%B8%E7%89%88%E6%9C%AC%E5%8E%86%E5%8F%B2)。
1007
1007
1008
+ ## 局部、全局、线程、CPU 变量的对比与使用
1009
+
1010
+ 在并发编程中,不同的变量有不同的使用场景和特点。以下是局部变量、全局变量、线程变量、CPU变量的对比:
1011
+
1012
+ > ### 局部变量(不考虑静态局部)
1013
+
1014
+ - 它拥有自动存储期,随作用域开始分配,结束时释放。每个线程、每次调用都有独立实例,完全独立,几乎无需同步。
1015
+
1016
+ > ### 全局变量
1017
+
1018
+ - 拥有***静态(static)***[存储期](https://zh.cppreference.com/w/cpp/language/storage_duration#.E5.AD.98.E5.82.A8.E6.9C.9F),它的存储在**程序**开始时分配,并在程序结束时解分配;且它在主函数执行之前进行[初始化](https://zh.cppreference.com/w/cpp/language/initialization#.E9.9D.9E.E5.B1.80.E9.83.A8.E5.8F.98.E9.87.8F)。
1019
+
1020
+ > ### 线程变量
1021
+
1022
+ - 拥有***线程(thread)***存储期。它的存储在**线程**开始时分配,并在线程结束时解分配。每个线程拥有它自身的对象实例。只有声明为 thread_local 的对象拥有此存储期(不考虑非标准用法)。它的初始化需要考虑局部与非局部两种情况:
1023
+ - 非局部变量:所有具有线程局部存储期的非局部变量的初始化,会**作为线程启动的一部分进行**,并按顺序早于线程函数的执行开始。
1024
+ - 静态局部变量[^4]:控制流首次**经过它的声明**时才会被初始化(除非它被[零初始化](https://zh.cppreference.com/w/cpp/language/zero_initialization)或[常量初始化](https://zh.cppreference.com/w/cpp/language/constant_initialization))。在其后所有的调用中,声明都会被跳过。
1025
+
1026
+ > ### CPU变量
1027
+
1028
+ - 它在标准 C++ 中无对应抽象实现,是操作系统内核功能。它主要依赖于当前系统内核来进行使用,也无法跨平台。基本概念与线程变量类似:CPU 变量是为每个处理器单独分配的变量副本。
1029
+
1030
+ ---
1031
+
1032
+ - 局部变量适合临时数据,作用域结束自动释放,几乎[^6]无需同步。
1033
+ - 全局变量适合整个程序的共享状态,但需要使用同步设施进行保护。
1034
+ - 线程变量适合线程的独立状态,通常[^5]无需同步。
1035
+ - CPU 变量的使用是少见的,主要用于内核开发和追求极致性能的高并发场景,减少 CPU 同步开销。
1036
+
1037
+ 总而言之,结合实际使用即可,把这四种变量拿出来进行对比,增进理解,加深印象。
1038
+
1039
+ [^4]: “[*静态局部变量*](https://zh.cppreference.com/w/cpp/language/storage_duration#.E9.9D.99.E6.80.81.E5.B1.80.E9.83.A8.E5.8F.98.E9.87.8F)”:是指拥有静态或线程存储期的局部变量。
1040
+
1041
+ [^5]: 之所以说“*通常*”而不是*一定*,是因为理论上线程变量一样可能产生数据竞争(例如有一个全局的指针指向了一个线程局部变量,其它线程通过这个指针读写线程局部变量而不附加同步,自然会产生数据竞争),只不过实践中通常不会那样做,所以我们不额外提及。
1042
+
1043
+ [^6]: 之所以说是“*几乎*”,是因为局部对象的构造、析构,或其它成员函数也可能修改共享数据、全局状态。如果它们不是线程安全的,同样可能产生数据竞争,例如,某类型 `X` 的构造函数会自增一个全局变量 `n`,那么即使局部对象本身是独立的,但由于构造函数修改了共享数据,依然会产生数据竞争。不过实践中这种情况较少,即使涉及到全局的状态,通常其本身也是线程安全的,例如前文提到的 `new`、`delete` 线程安全的问题。
1044
+
1008
1045
## 总结
1009
1046
1010
1047
本章讨论了多线程的共享数据引发的恶性条件竞争会带来的问题。并说明了可以使用互斥量(`std::mutex`)保护共享数据,并且要注意互斥量上锁的“**粒度**”。C++标准库提供了很多工具,包括管理互斥量的管理类(`std::lock_guard`),但是互斥量只能解决它能解决的问题,并且它有自己的问题(**死锁**)。同时我们讲述了一些避免死锁的方法和技术。还讲了一下互斥量所有权转移。然后讨论了面对不同情况保护共享数据的不同方式,使用 `std::call_once()` 保护共享数据的初始化过程,使用读写锁(`std::shared_mutex`)保护不常更新的数据结构。以及特殊情况可能用到的互斥量 `recursive_mutex`,有些人可能喜欢称作:**递归锁**。最后聊了一下 `new`、`delete` 运算符的库函数实际是线程安全的,以及线程存储期。
You can’t perform that action at this time.
0 commit comments