网站首页 > 基础教程 正文
前言
在内存模型之上,C++提供了线程加锁的并发模型来消除数据竞争(比如全局数据。数据竞争可能产生极为隐晦的并发错误)。但是,线程加锁级别的并发是应用程序使用并发的比较差的模型。
C++11引入了thread_local来保证线程访问全局数据的安全,thread_local告诉线程进行数据本地存储(线程本地存储一个数据备份),从而消除线程间数据竞争的一种方式。
thread_local概念
thread_local是C++11引入的新特性,是一个关键字,用于修饰变量(thread_local关键字和static、extern关键字在使用上是不冲突的),告诉线程对此关键词修饰的数据进行本地存储。
使用的头文件:#include< thread>
thread_local与“线程加锁”的区别
thread_local:把全局数据拷贝出一份自己使用,从进程内部所有线程共用变成线程内部私有。随后线程使用的变量就是这个备份数据,不再关心原全局数据的数值。
(进程内部的数据在那里,只是系统给当前线程分配了同样类型同样名称的变量,专供本线程使用。)
线程加锁的方式:全局数据进程内部所有线程共用,每个线程都可以对这个数据进行读和写,为了保证数据同一时间只能有一个修改,所以加锁进行限制。
thread_local应用场景
为了防止变量值的不可预测性,保证各线程内变量值的互不干扰,想要达到线程内独有的全局变量的场合。
举例(仅供参考):
①修饰全局业务唯一性的某个全局变量A,在fun_A函数中进行A的赋值(比如,随机数、随机串、线程池个数等赋值计算)。
以前当前进行内部所有线程都共用A和fun_A,但是根据业务扩展需要,每个线程内部都需要维护一份业务唯一性数据。但是业务特殊性导致线程之间业务差异比较大,无法通过拷贝多个进程实现多种业务。
所以为了修改方便,可以选择对A的变量增加thread_local修饰的方案。
thread_local的使用范例
thread_local主要是用来修饰变量,可以修饰变量的种类有以下四种:
1)全局变量
2)局部变量
3)类对象
4)类成员变量
下面我们以全局变量和局部变量为例,来说明thread_local修饰变量的效果。
示例代码:
#include <iostream>
#include <thread>
int g_a = 10;
thread_local int g_b = 20;
static int g_c = 30;
thread_local static int g_d = 40;
void fun(const std::string& name)
{
for (int i = 0; i < 3; ++i)
{
thread_local int temp = 1; //只在每个线程创建时初始化一次
temp++;
std::cout << " thread[" << name << "]: & temp=" << &temp << " temp =" << temp << std::endl;
}
for (int j = 0; j < 3; ++j)
{
thread_local static int s_temp_2 = 1; //只在每个线程创建时初始化一次
s_temp_2++;
std::cout << " thread[" << name << "]: &s_temp_2=" << &s_temp_2 << " s_temp_2 =" << s_temp_2 << std::endl;
}
}
void test_fun(const std::string& name)
{
std::cout << std::endl << name << " 子线程ID :" << std::this_thread::get_id() << std::endl;
if (name == "t2")//在t2线程定义变量,用以区分不同线程的内存分配地址
{
int t_2 = 5;
std::cout << " 局部变量 &t_2=" << &t_2 << " t_2=" << t_2 << std::endl;
}
//从子线程查看数据的地址
g_a++;
g_b++;
g_c++;
g_d++;
std::cout << "&g_a=" << &g_a << " g_a=" << g_a << std::endl;
std::cout << "&g_b=" << &g_b << " g_b=" << g_b << " <-thread_local" << std::endl;
std::cout << "&g_c=" << &g_c << " g_c=" << g_c << std::endl;
std::cout << "&g_d=" << &g_d << " g_d=" << g_d << " <-thread_local" << std::endl;
fun(name);//测试局部数据
}
int main()
{
std::cout << "主线程ID :" << std::this_thread::get_id() << std::endl;
int temp = 5;
std::cout << "局部变量 &temp=" << &temp << " temp=" << temp << std::endl;
//查看主线程数据的地址
std::cout << "&g_a=" << &g_a << " g_a=" << g_a << std::endl;
std::cout << "&g_b=" << &g_b << " g_b=" << g_b << " <-thread_local" << std::endl;
std::cout << "&g_c=" << &g_c << " g_c=" << g_c << std::endl;
std::cout << "&g_d=" << &g_d << " g_d=" << g_d << " <-thread_local" << std::endl;
std::thread t1(test_fun, "t1");
//为了方便通过屏幕输出查看结果,通过sleep防止不同线程输出顺序错乱
std::this_thread::sleep_for(std::chrono::seconds(1));
std::thread t2(test_fun, "t2");
//等待线程处理结束
t1.join();
t2.join();
return 0;
}
运行结果:
结论:
1.编译器把thread_local修饰的变量(全局、静态全局、局部、静态局部等被修饰的变量)都放在一个专门的空间(不是进程的全局区,也不是局部变量区域)进行存储。比如图中的黄色部分,都分配在15BC52Exxxx区间。
2.thread_local修饰的全局变量,主线程使用单独的一份内存(比如图中的红色部分),所有子线程使用共同的内存(比如图中的蓝色部分)。
3.thread_local修饰的变量,数值被隔离、被备份,即数据是当前线程内部私有,不会和其他线程相互影响。(线程退出会销毁这些"被备份"的数据)
4.thread_local修饰的变量,在主线程中初始化。
5.局部变量的thread_local和局部static变量效果很相似,第一次运行时候进行初始化,从第二次以后就跳过初始化,直接使用该变量。局部thread_local的生命周期是在线程启动时候进行初始化,到线程退出时候进行析构销毁。
补充:进程和线程的区别
①概念角度
对操作系统来说,进程是资源分配的基本单位,而线程则是任务调度的基本单位。
内核中的任务调度实际是在调度线程,进程只是给线程提供虚拟内存、全局变量等资源。
线程产生的原因:进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;但是其具有一些缺点:
进程在同一时间只能干一件事。进程在执行的过程中如果阻塞,整个进程就会挂起,即使进程中有些工作不依赖于等待的资源,仍然不会执行。
因此,操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时空开销,提高并发性。
②上下文切换角度
线程上下文切换时,共享相同的虚拟内存和全局变量等资源不需要修改。而线程自己的私有数据,如栈和寄存器等,上下文切换时需要保存。
当进程中只有一个线程时,可以认为进程就等于线程。
当进程拥有多个线程时,这些线程会共享父进程的资源(即共享相同的虚拟内存和全局变量等资源)。这些资源在上下文切换时是不需要修改的。
另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
因此线程上下文切换有两种情况:
·前后两个线程属于不同进程,因为资源不共享,所以切换过程就跟进程上下文切换是一样的;
·前后两个线程属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
因为进程切换,消耗的资源大。所以涉及到频繁的切换,使用线程要好于进程。
③内存角度
同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间;
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃会导致整个进程崩溃,所以多进程比多线程健壮。
每个独立的进程有一个程序的入口、程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
线程内私有:线程栈,寄存器,程序寄存器
线程间共享:堆,地址空间,全局变量,静态变量
进程内私有:地址空间,堆,全局变量,栈,寄存器
④使用场景
需要频繁创建销毁的优先用线程。比如,Web 服务器来一个连接建立一个线程,连接断了就销毁线程
需要进行大量计算的优先使用线程。所谓大量计算,就是要耗费很多 CPU,切换频繁,这种情况下线程是最合适的。最常见的是图像处理、算法处理。
强相关的处理用线程,弱相关的处理用进程。
补充:进程内存空间的分布
进程的虚拟地址空间图示如下:
栈段:
1. 为函数内部的局部变量提供存储空间。
2. 进行函数调用时,存储“过程活动记录”。
3. 用作暂时存储区。如计算一个很长的算术表达式时,可以将部分计算结果压入堆栈。
堆段:
程序通过malloc或new自己开辟的存储空间。
数据段:
BSS段存储未初始化或初始化为0的全局变量、静态变量。数据段存储经过初始化的全局和静态变量。
代码段:
又称为文本段。存储可执行文件的指令;也有可能包含一些只读的常数变量,例如字符串常量等。
猜你喜欢
- 2024-11-11 Linux下的C++ socket编程实例 linux c++ tcp
- 2024-11-11 C++11原子变量:线程安全、无锁操作的实例解析
- 2024-11-11 知识重构-c++ : Lambda 知识重构拼音
- 2024-11-11 c++ 疑难杂症(4) std:vector c++ vector subscript out of range
- 2024-11-11 深入探索C++异步编程的奥秘 c++11异步编程
- 2024-11-11 C++ 开发中使用协程需要注意的问题
- 2024-11-11 golang极速嵌入式Linux应用开发(四)-协程与并发
- 2024-11-11 在计算机编程中,线程是指一个程序内部的执行流程
- 2024-11-11 C++ std:decay、std:bind、std:packaged_task 在模版编程的实践
- 2024-11-11 Linux/C++简单线程池实现 了解Java语言对于多线程的支持多丰富
- 最近发表
- 标签列表
-
- gitpush (61)
- pythonif (68)
- location.href (57)
- tail-f (57)
- pythonifelse (59)
- deletesql (62)
- c++模板 (62)
- css3动画 (57)
- c#event (59)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- exec命令 (59)
- canvasfilltext (58)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- node教程 (59)
- console.table (62)
- c++time_t (58)
- phpcookie (58)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)