丝路幽径:穿梭于Linux多线程控制的秘境
文章目录
- 🌇前言
- 🏙️正文
- 1、线程知识补充
- 1.1、线程私有资源
- 1.3、原生线程库
- 2、线程控制接口
- 2.1、线程创建
- 2.2、线程等待
- 2.3、线程终止
- 2.4、线程实战
- 2.5、其他接口
- 2.5.1、关闭线程
- 2.5.2、获取线程ID
- 2.5.3、线程分离
- 3、深入理解线程
- 3.1、理解线程库及线程 ID
- 3.2、理解线程独立栈
- 3.3、理解线程局部存储
- 🌙结语 · 掌控线程,如驭星辰

🌇前言
线程是进程内部的一个执行流,作为 CPU
运行的基本单位,对于线程的合理控制与任务的执行效率息息相关,因此掌握线程基本操作(线程控制)是很有必要的
🏙️正文
1、线程知识补充
在正式介绍线程控制相关接口前,需要先补充一波线程相关知识
1.1、线程私有资源
在上篇线程初识中我们得出了一个结论:Linux 中没有真线程,只有复用 PCB 设计思想的 TCB 结构
显然,多线程虽然共同 “生活” 在一个进程中,但也需要有自己的 “隐私”,而这正是 线程私有资源
线程私有资源:
线程 ID
:内核观点中的 LWP一组寄存器
: 线程切换时,当前线程的上下文数据需要被保存线程独立栈
: 线程在执行函数时,需要创建临时变量错误码 errno
: 线程因错误终止时,需要告知父进程信号屏蔽字
: 不同线程对于信号的屏蔽需求不同调度优先级
: 线程也是要被调度的,需要根据优先级进行合理调度
其中,线程 最重要
的资源是 一组寄存器
(体现切换特性)和独立栈
(体现临时运行特性)
这两个资源共同构成了最基本的线程
1.2、线程共享资源
除了上述提到的 线程私有资源
外,多线程还共享着进程中的部分资源
共享的定义:不需要太多的额外成本,就可以实现随时访问资源
基于 多线程看到的是同一块进程地址空间,理论上 凡是在进程地址空间中出现的资源,多线程都是可以看到的
但实际上为了确保线程调度、运行时的独立性,只能共享部分资源
这也就是线程中的栈区称作 “
独立栈
” 的原因:某块栈空间属于某个线程,其他线程是可以访问的,为了确保独立性,并不会这样做
在 进程地址空间
中,诸如 共享区、全局数据区
等 这类天生自带共享属性的区域支持 多线程共享
在 Linux 中,多线程共享资源如下
线程共享资源:
- 共享区、全局数据区、字符常量区、代码区,常规资源共享区
- 文件描述符表: 进行 IO 操作时,无需再次打开文件
- 每种信号的处理方式: 多线程共同构成一个整体,信号的处理动作必须统一
- 当前工作目录: 即使是多线程,也是位于同一工作目录下
- 用户 ID 和 组 ID: 进程属于某个组中的某个用户,多线程也是如此
其中,线程 较重要
的共享资源是:文件描述符表
涉及 IO
操作时,多线程 多路转接
非常实用
进程和线程关系图示
多个单线程进程
和 单进程多线程
比较常用
1.3、原生线程库
在之前编译多线程相关代码时,我们必须带上一个选项:-lpthread
,否则就无法使用多线程相关接口
带上这个选项的目的很简单:使用 pthread
原生线程库
接下来对 原生线程库
进行一个系统性的理解
-
首先,在 Linux 中是没有真正意义上的线程的,有的只是通过进程模拟实现的线程(LWP)
-
站在操作系统角度:并不会提供对线程控制的相关接口,最多提供轻量级进程操作的相关接口
-
但是对于用户来说,只认识线程,并不清楚轻量级进程
-
所以为了使用户能愉快的对线程进行操作,就需要对系统提供的轻量级进程操作相关接口进行封装:对下封装轻量级进程操作相关接口,对上给用户提供线程控制的相关接口
这里很好的体现了计算机界的哲学:通过添加一层软件层
解决问题
在 Linux
中,封装轻量级进程操作相关接口的库称为 pthread
库,即 原生线程库
,这个库文件是所有 Linux 系统都必须预载的,用户使用多线程控制相关接口时,只需要指明使用-lpthread
库,即可正常使用多线程控制相关接口
2、线程控制接口
有了前面知识的补充之后,接下来正式进入线程控制接口的学习
2.1、线程创建
要想控制线程,得先创建线程,对于 原生线程库 来说,创建线程使用的是 pthread_create
这个接口
#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
先来认识一下函数中涉及的参数
参数1 pthread_t*:线程 ID,用于标识线程,其实这玩意本质上就是一个 unsigned long int
类型
注:pthread_t*
表明这是一个输出型参数,旨在创建线程后,获取新线程 ID
参数2 const pthread_attr_t*
:用于设置线程的属性,比如优先级、状态、私有栈大小,这个参数一般不考虑,直接传递 nullptr
使用默认设置即可
参数3 void *(start_routine) (void ):这是一个很重要的参数,它是一个 返回值为 void 参数也为 void 的函数指针,线程启动时,会自动回调此函数(类似于 signal 函数中的参数2)
参数4 void*:显然,这个类型与回调函数中的参数类型匹配上了,而这正是线程运行时,传递给回调函数的参数
返回值 int:创建成功返回 0,失败返回 error number
明白创建线程函数的各个参数后,就可以尝试创建一个线程了
#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;void* threadRun(void *arg)
{while(true){cout << "我是次线程,我正在运行..." << endl;sleep(1);}return nullptr;
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);while(true){cout << "我是主线程 " << " 我创建了一个次线程 " << t << endl;sleep(1);}return 0;
}
非常简单的代码,此时如果直接编译会引发报错
-
错误:未定义 pthread_create 这个函数
-
原因:没有指明使用
原生线程库
,这是一个非常常见的问题 -
解决方法:编译时带
上 -lpthread
,指明使用 原生线程库
此时再编译就没有问题了
可以通过ps -aL
查看正在运行中的线程信息
接下来解决一批衍生问题
1.如何验证 原生线程库
存在?
现在我们已经得到了一个链接 原生线程库 的可执行程序,可以通过 ldd 可执行程序
查看库的链接情况
可以看到,原生线程库路径: /lib64/libpthread.so.0
足以证明原生线程库确确实实的存在于我们的系统中
2.为什么打印的次线程 ID
如此长?并且与 ps -aL
查出来的 LWP
不一致?
很长是因为它本质上是一个无符号长整型,至于为什么显示不一致的问题,需要到后面才能解答
3.程序运行时,主次线程的运行顺序?
线程的调度机制源于进程,而多进程运行时,谁先运行取决于调度器,因此主次线程运行的先后顺序不定,具体取决于调度器的调度
2.1.1、一批线程
接下来演示创建一批线程
#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name)
{while(true){cout << "我是次线程 " << (char*)name << endl;sleep(1);}return nullptr;
}int main()
{pthread_t pt[NUM];for(int i = 0; i < NUM; i++){// 注册新线程的信息char name[64];snprintf(name, sizeof(name), "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}while(true){cout << "我是主线程,我正在运行" << endl;sleep(1);}return 0;
}
预期结果:打印 thread-1、thread-2、thread-3 …
实际结果:确实有五个次线程在运行,但打印的结果全是 thread-5
原因:char name[64] 属于主线程中栈区之上的变量,多个线程实际指向的是同一块空间,最后一次覆盖后,所有线程都打印 thread-5
这是由于多线程共享同一块区域引发的问题,解决方法就是在堆区动态匹配空间,使不同的线程读取不同的空间,这样就能确保各自信息的独立性
#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name)
{while(true){cout << "我是次线程 " << (char*)name << endl;sleep(1);}delete[] (char*)name;return nullptr;
}int main()
{pthread_t pt[NUM];for(int i = 0; i < NUM; i++){// 注册新线程的信息char *name = new char[64];snprintf(name, 64, "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}while(true){cout << "我是主线程,我正在运行" << endl;sleep(1);}return 0;
}
现在程序能符合预期般运行了
显然,线程每次的运行顺序取决于调度器
在上面的程序中,主线程也是在死循环式运行,假若主线程等待 3 秒后,再 return, 会发生什么呢?
是因为 主线程结束了,整个进程的资源都得被释放,次线程自然也就无法继续运行了
- 换句话说,次线程由主线程创建,主线程就得对他们负责,必须等待他们运行结束,类似于父子进程间的
等待
机制; - 如果不等待,就会引发僵尸进程问题,不过线程这里没有僵尸线程的概念,直接影响就是次线程也全部退出了
2.2、线程等待
主线程需要等待次线程,在 原生线程库
中刚好存在这样一个接口 pthread_join
,用于等待次线程运行结束
#include <pthread.h>int pthread_join(pthread_t thread, void **retval);
照例先来看看参数部分
-
参数1 pthread_t:待等待的线程 ID,本质上就是一个无符号长整型类型;这里传递是数值,并非地址
-
参数2 void**:这是一个输出型参数,用于获取次线程的退出结果,如果不关心,可以传递 nullptr
-
返回值:成功返回 0,失败返回 error number
函数原型很简单,使用也很简单,我们可以直接在主线程中调用并等待所有次线程运行结束
#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name)
{while(true){cout << "我是次线程 " << (char*)name << endl;sleep(1);}delete[] (char*)name;return nullptr;
}int main()
{pthread_t pt[NUM];for(int i = 0; i < NUM; i++){// 注册新线程的信息char *name = new char[64];snprintf(name, 64, "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}// 等待次线程运行结束for(int i = 0; i < NUM; i++){int ret = pthread_join(pt[i], nullptr);if(ret != 0)cerr << "等待线程 " << pt[i] << " 失败!" << endl;}cout << "所有线程都退出了" << endl;return 0;
}
主线程需要等待次线程运行结束,整个程序也就正常运行了
2.3、线程终止
线程可以被创建并运行,也可以被终止,线程终止方式有很多种
比如 等待线程回调函数执行结束,次线程运行五秒后就结束了,然后被主线程中的 pthread_join
等待成功,次线程使命完成
void* threadRun(void *name)
{// 只让次线程运行五秒int n = 5;while(n--){cout << "我是次线程 " << (char*)name << endl;sleep(1);}delete[] (char*)name;return nullptr;
}
还有一种方法是 在次线程回调方法中调用 exit() 函数
,但这会引发一个大问题:只要其中一个线程退出了,其他线程乃至整个进程都得跟着退出,显然这不是很合理,不推荐这样玩多线程
void* threadRun(void *name)
{while(true){cout << "我是次线程 " << (char*)name << endl;sleep(1);// 直接终止进程,退出码设为 10exit(10);}delete[] (char*)name;return nullptr;
}
每个线程顶多存活一秒(存活在同一秒中)就被终止了,通过 echo $?
查询最近一次退出码,正是 10
其实 原生线程库 中有专门终止线程运行的接口 pthread_exit
,专门用来细粒度地终止线程,谁调用就终止谁,不会误伤其他线程
#include <pthread.h>void pthread_exit(void *retval);
仅有一个参数 void
*:用于传递线程退出时的信息
这个参数名叫 retval,pthread_join 中的参数2也叫 retval,两者有什么不可告人的秘密吗?
- 答案是这俩其实本质上是同一个东西,
pthread_join
中的void **retval
是一个输出型参数,可以把一个void *
指针的地址传递给 pthread_join 函数 - 当线程调用
pthread_exit
退出时,可以根据此地址对retval
赋值,从而起到将退出信息返回给主线程的作用
为什么 pthread_join
中的参数2类型为 void**
?
- 因为主线程和次线程此时并不在同一个栈帧中,要想远程修改值就得传地址,类似于 int -> &int,不过这里的
retval
类型是void*
注意: 直接在 回调方法
中 return
退出信息,主线程中的 retval
也是可以得到信息的,因为类型都是 void*
,彼此相互呼应
所以比较完善的多线程操作应该是这样的:
#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name)
{cout << "我是次线程 " << (char*)name << endl;sleep(1);delete[] (char*)name;pthread_exit((void*)"EXIT");// 直接return "EXIT" 也是可以的// return (void*)"EXIT";
}int main()
{pthread_t pt[NUM];for(int i = 0; i < NUM; i++){// 注册新线程的信息char *name = new char[64];snprintf(name, 64, "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}// 等待次线程运行结束void *retval = nullptr;for(int i = 0; i < NUM; i++){int ret = pthread_join(pt[i], &retval);if(ret != 0)cerr << "等待线程 " << pt[i] << " 失败!" << endl;cout << "线程 " << pt[i] << " 等待成功,退出信息是 " << (const char*)retval << endl;}cout << "所有线程都退出了" << endl;return 0;
}
void*
非常之强大,可以指向任意类型的数据,甚至是一个对象
既然线程复用进程的设计思想,为什么线程退出时不需要考虑是否正常退出、错误码是什么之类的?
- 因为线程是进程的一部分,在进程中获取线程的错误信息等是无意义的
- 前面说过,如果一个线程因错误而被终止了,那么整个进程也就都活不了了,错误信息甄别交给父进程去完成,因此
pthread_join
就没必要关注线程退出时的具体状态了; - 如果次线程有信息要交给主线程,可以通过
retval
输出型参数获取
2.4、线程实战
无论是 pthread_create
还是 pthread_join
,他们的参数都有一个共同点:包含了一个 void*
类型的参数,这就是意味着我们可以给线程传递对象,并借此进行某种任务处理
比如我们先创建一个包含一下信息的线程信息类,用于计算 [0, N] 的累加和
- 线程名字(包含 ID)
- 线程编号
- 线程创建时间
- 待计算的值 N
- 计算结果
- 状态
为了方便访问成员,权限设为 public
// 线程信息类的状态
enum class Status
{OK = 0,ERROR
};// 线程信息类
class ThreadData
{
public:ThreadData(const string &name, int id, int n):_name(name),_id(id),_createTime(time(nullptr)),_n(n),_result(0),_status(Status::OK){}public:string _name;int _id;time_t _createTime;int _n;int _result;Status _status;
};
此时就可以编写 回调方法
中的业务逻辑了
void* threadRun(void *arg)
{ThreadData *td = static_cast<ThreadData*>(arg);// 业务处理for(int i = 0; i <= td->_n; i++)td->_result += i;// 如果业务处理过程中发现异常行为,可以设置 _status 为 ERRORcout << "线程 " << td->_name << " ID " << td->_id << " CreateTime " << td->_createTime << " done..." << endl;pthread_exit((void*)td);// 也可以直接 return // return td;
}
主线程在创建线程及等待线程时,就可以使用 ThreadData
对象了,后续涉及业务修改时,也只需要修改类及回调方法即可,无需再更改创建及等待逻辑,有效做到了 解耦
int main()
{pthread_t pt[NUM];for(int i = 0; i < NUM; i++){// 注册新线程的信息char name[64];snprintf(name, sizeof(name), "thread-%d", i + 1);// 创建对象ThreadData *td = new ThreadData(name, i, 100 * (10 + i));pthread_create(pt + i, nullptr, threadRun, td);sleep(1); // 尽量拉开创建时间}// 等待次线程运行结束void *retval = nullptr;for(int i = 0; i < NUM; i++){int ret = pthread_join(pt[i], &retval);if(ret != 0)cerr << "等待线程 " << pt[i] << " 失败!" << endl;ThreadData *td = static_cast<ThreadData*>(retval);if(td->_status == Status::OK)cout << "线程 " << pt[i] << " 计算 [0, " << td->_n << "] 的累加和结果为 " << td->_result << endl;delete td;}cout << "所有线程都退出了" << endl;return 0;
}
程序可以正常运行,各个线程也都能正常计算出结果;这里只是简单计算累加和,线程还可以用于其他场景:网络传输、密集型计算、多路 IO等,无非就是修改线程的业务逻辑
结论:多线程可以传递对象指针,自由进行任务处理
2.5、其他接口
与多线程相关的还有一批其他接口,比较简单,就放在一起介绍了
2.5.1、关闭线程
线程可以被创建,自然也可以被关闭,可以使用 pthread_cancel
关闭已经创建并运行中的线程
#include <pthread.h>int pthread_cancel(pthread_t thread);
-
参数1 pthread_t:被关闭的线程 ID
-
返回值:成功返回 0,失败返回一个非零的
error number
这里可以直接模拟关闭线程的场景
#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadRun(void *arg)
{const char *ps = static_cast<const char*>(arg);while(true){cout << "线程 " << ps << " 正在运行" << endl;sleep(1);}pthread_exit((void*)10);
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, (void*)"Hello Thread");// 3秒后关闭线程sleep(3);pthread_cancel(t);void *retval = nullptr;pthread_join(t, &retval);// 细节:使用 int64_t 而非 uint64_tcout << "线程 " << t << " 已退出,退出信息为 " << (int64_t)retval << endl;return 0;
}
原因很简单:只要是被pthread_cancel
关闭的线程,退出信息统一为PTHREAD_CANCELED
即 -1
这也就解释了为什么要强转为 ingt64_t
,因为无符号的 -1
非常大,不太好看
2.5.2、获取线程ID
线程 ID
是线程的唯一标识符,可以通过 pthread_self
获取当前线程的 ID
#include <pthread.h>pthread_t pthread_self(void);
返回值:当前线程的 ID
#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadRun(void *arg)
{cout << "当前次线程的ID为 " << pthread_self() << endl;return nullptr;
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);pthread_join(t, nullptr);cout << "创建的次线程ID为 " << t << endl;return 0;
}
可以看到结果都是一样的
2.5.3、线程分离
父进程需要阻塞式等待子进程退出,主线程等该次线程时也是阻塞式等待,父进程可以设置为 WNOHANG
,变成轮询式等待,避免自己一直处于阻塞;次线程该如何做才能避免等待时阻塞呢?
答案是 分离 Detach
线程在被创建时,默认属性都是
joinable
的,即主线程需要使用pthread_join
来等待次线程退出,并对其进行资源释放;实际上我们可以把这一操作留给系统自动处理,如此一来主线程就可以不必等待次线程,也就可以避免等待时阻塞了,这一操作叫做线程分离
原生线程库
提供的线程分离接口是 pthread_detach
-
参数1 pthread_t:待分离的线程
ID
-
返回值:成功返回 0,失败返回
error number
线程分离的本质是将joinable
属性修改为 detach
,告诉系统线程退出后资源自动释放
注意: 如果线程失去了 joinable
属性,就无法被 join
,如果 join
就会报错
接下来简单使用一下 线程分离
#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadRun(void *arg)
{int n = 3;while(n){cout << "次线程 " << n-- << endl;sleep(1);}
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);pthread_detach(t);int n = 5;while(n){cout << "主线程 " << n-- << endl;sleep(1);}return 0;
}
主线程可以不用等待次线程,两个执行流并发运行,并且不必担心次线程出现僵尸问题
建议将 pthread_detach
放在待分离线程的 线程创建
语句之后,如果放在线程执行函数中,可能会因为调度优先级
问题引发错误(未知结果)
- 线程被创建后,谁先执行不确定
- 总之,线程被分离后,主线程就可以不必关心了,即不需要
join
等待,是否分离线程取决于具体的应用场景
3、深入理解线程
3.1、理解线程库及线程 ID
在见识过 原生线程库
提供的一批便利接口后,不由得感叹库的强大,如此强大的库究竟是如何工作的呢?
原生线程库本质上也是一个文件,是一个存储在 /lib64 目录下的动态库,要想使用这个库,就得在编译时带上 -lpthread
指明使用动态库
程序运行时,原生线程库 需要从 磁盘
加载至 内存
中,再通过 进程地址空间
映射至 共享区
中供线程使用
由于用户并不会直接使用轻量级进程
的接口,于是 需要借助第三方库进行封装,类似于用户可能不了解系统提供的 文件接口
,从而使用 C语言
封装的 FILE 库
一样
对于 原生线程库
来说,线程不止一个,因此遵循 先描述,再组织
原则,在线程库中创建TCB
结构(类似于 PCB),其中存储 线程
的各种信息,比如 线程独立栈
信息
在内存中,整个 线程库
就像一个 “数组”
,其中的一块块空间聚合排布 TCB
信息,而 每个 TCB
的起始地址就表示当前线程的 ID,地址是唯一的,因此线程 ID 也是唯一的
因此,我们之前打印 pthread_t
类型的 线程 ID 时,实际打印的是地址,不过是以十进制
显示的,可以通过函数将地址转化为使用 十六进制
显示
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), "0x%x", t);return id;
}void *threadRun(void *arg)
{cout << "我是[次线程],我的ID是 " << toHex(pthread_self()) << endl;return (void*)0;
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);pthread_join(t, nullptr);cout << "我是[主线程],我的ID是 " << toHex(pthread_self()) << endl;return 0;
}
线程ID
确实能转化为地址(虚拟进程地址空间上的地址)
注意: 即便是 C++11 提供的 thread 线程库,在 Linux
平台中运行时,也需要带上-lpthread
选项,因为它本质上是对 原生线程库
的封装
3.2、理解线程独立栈
线程 之间存在 独立栈
,可以保证彼此之间执行任务时不会相互干扰,可以通过代码证明
多个线程使用同一个入口函数,并打印其中临时变量的地址
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), "0x%x", t);return id;
}void *threadRun(void *arg)
{int tmp = 0;cout << "thread " << toHex(pthread_self()) << " &tmp: " << &tmp << endl;return (void*)0;
}int main()
{pthread_t t[5];for(int i = 0; i < 5; i++){pthread_create(t + i, nullptr, threadRun, nullptr);sleep(1);}for(int i = 0; i < 5; i++)pthread_join(t[i], nullptr);return 0;
}
可以看到五个线程打印 “同一个” 临时变量的地址并不相同,足以证明 线程独立栈
的存在
存在这么多 栈结构
,CPU 在运行时是如何区分的呢?
- 答案是 通过
栈顶指针 ebp
和栈底指针 esp
进行切换 - ebp 和 esp 是 CPU 中两个非常重要的
寄存器
,即便是程序启动,也需要借助这两个寄存器
为 main 函数开辟对应的栈区
除了移动
esp
扩大栈区外,还可以同时移动 ebp
和 esp
更改当前所处栈区
所以,多线程中 独立栈
可以通过 ebp
和 esp
轻松切换并使用
如果想要在栈区中开辟整型空间,可以使用
ebp - 4
定位对应的空间区域并使用,其他类型也是如此,原理都是基地址 + 偏移量
注意:
- 所有线程都要有自己独立的栈结构(独立栈),主线程中用的是进程系统栈,次线程用的是库中提供的栈
- 多个线程调用同一个入口函数(回调方法),其中的局部变量地址一定不一样,因为存储在
线程独立栈
中
3.3、理解线程局部存储
线程
之间共享 全局变量
,对 全局变量
进行操作时,会影响其他线程
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;int g_val = 100;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), "0x%x", t);return id;
}void *threadRun(void *arg)
{cout << "thread: " << toHex(pthread_self()) << " g_val: " << ++g_val << " &g_val: " << &g_val << endl;return (void*)0;
}int main()
{pthread_t t[3];for(int i = 0; i < 3; i++){pthread_create(t + i, nullptr, threadRun, nullptr);sleep(1);}for(int i = 0; i < 3; i++)pthread_join(t[i], nullptr);return 0;
}
在三个线程的影响下,g_val
最终变成了 103
如何让全局变量私有化呢?即每个线程看到的全局变量不同
可以给全局变量加 __thread
修饰,修饰之后,全局变量不再存储至全局数据区,而且存储至线程的 局部存储区
中
__thread int g_val = 100;
结果:修饰之后,每个线程确实看到了不同的 “全局变量
”
特点:此时的 “全局变量
” 的地址变大了
- “全局变量” 地址变大是因为此时它不再存储在
全局数据区
中,而且存储在线程的局部存储区
中 - 线程的局部存储区位于
共享区
,并且共享区
的地址天然大于全局数据区
注意: 局部存储区位于共享区中,可以通过__thread
修饰来改变变量的存储位置
🌙结语 · 掌控线程,如驭星辰
Linux 多线程的世界宏大而微妙,它带来了并行计算的强大能力,也带来了同步、互斥等挑战。线程控制,就像是乐队指挥,在各个声部间调度有序,和谐共鸣。
在现代系统的运行核心中,在高性能计算的神经网络里,多线程已成为那奔腾不息的光之洪流,唯有理解与掌控,才能在这星海中自由航行。
本篇关于线程控制的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持斧正!!!