关于c++:线程间到底共享了哪些进程资源

64次阅读

共计 3171 个字符,预计需要花费 8 分钟才能阅读完成。

过程和线程这两个话题是程序员绕不开的,操作系统提供的这两个抽象概念切实是太重要了。

对于过程和线程有一个 极其经典 的问题,那就是过程和线程的区别是什么?置信很多同学对答案似懂非懂。

记住了不肯定真懂

有的同学可能曾经“背得”滚瓜烂熟了:“过程是操作系统分配资源的单位,线程是调度的根本单位,线程之间共享过程资源”。

可是你真的了解了下面这句话吗?到底线程之间共享了哪些过程资源,共享资源意味着什么?共享资源这种机制是如何实现的?对此如果你没有答案的话,那么这意味着 你简直很难写出能正确工作的多线程程序,同时也意味着这篇文章就是为你筹备的。

逆向思考

查理芒格常常说这样一句话:“反过来想,总是反过来想”,如果你对线程之间共享了哪些过程资源这个问题想不分明的话那么也能够反过来思考,那就是 有哪些资源是线程公有的

线程公有资源

线程运行的实质其实就是函数的执行,函数的执行总会有一个源头,这个源头就是所谓的入口函数,CPU 从入口函数开始执行从而造成一个执行流,只不过咱们人为的给执行流起一个名字,这个名字就叫线程。

既然线程运行的实质就是函数的执行,那么函数执行都有哪些信息呢?

在《函数运行时在内存中是什么样子?》这篇文章中咱们说过,函数运行时的信息保留在栈帧中,栈帧中保留了函数的返回值、调用其它函数的参数、该函数应用的局部变量以及该函数应用的寄存器信息,如图所示,假如函数 A 调用函数 B:

此外,CPU 执行指令的信息保留在一个叫做程序计数器的寄存器中,通过这个寄存器咱们就晓得接下来要执行哪一条指令。因为操作系统随时能够暂停线程的运行,因而咱们保留以及恢复程序计数器中的值就能晓得线程是从哪里暂停的以及该从哪里持续运行了。

因为线程运行的实质就是函数运行,函数运行时信息是保留在栈帧中的,因而每个线程都有本人独立的、公有的栈区。

同时函数运行时须要额定的寄存器来保留一些信息,像局部局部变量之类,这些寄存器也是线程公有的,一个线程不可能拜访到另一个线程的这类寄存器信息

从下面的探讨中咱们晓得,到目前为止,所属线程的栈区、程序计数器、栈指针以及函数运行应用的寄存器是线程公有的。

以上这些信息有一个对立的名字,就是 线程上下文,thread context。

咱们也说过操作系统调度线程须要随时中断线程的运行并且须要线程被暂停后能够持续运行,操作系统之所以能实现这一点,依附的就是线程上下文信息。

当初你应该晓得哪些是线程公有的了吧。

除此之外,剩下的都是线程间共享资源。

那么剩下的还有什么呢?还有图中的这些。

这其实就是过程地址空间的样子,也就是说线程共享过程地址空间中除线程上下文信息中的所有内容,意思就是说线程能够 间接读取 这些内容。

接下来咱们别离来看一下这些区域。

代码区

过程地址空间中的代码区,这里保留的是什么呢?从名字中有的同学可能曾经猜到了,没错,这里保留的就是咱们写的代码,更精确的是编译后的可执行机器指令

那么这些机器指令又是从哪里来的呢?答案是从可执行文件中加载到内存的,可执行程序中的代码区就是用来初始化过程地址空间中的代码区的。

线程之间共享代码区,这就意味着程序中的任何一个函数都能够放到线程中去执行,不存在某个函数只能被特定线程执行的状况

堆区

堆区是程序员比拟相熟的,咱们在 C /C++ 中用 malloc 或者 new 进去的数据就寄存在这个区域,很显然,只有晓得变量的地址,也就是指针,任何一个线程都能够拜访指针指向的数据,因而堆区也是线程共享的属于过程的资源。

栈区

唉,等等!刚不是说栈区是线程公有资源吗,怎么这会儿又说起栈区了?

的确,从线程这个形象的概念上来说,栈区是线程公有的,然而从理论的实现上看,栈区属于线程公有这一规定并没有严格遵守,这句话是什么意思?

通常来说,留神这里的用词是 通常,通常来说栈区是线程公有,既然有通常就有不通常的时候。

不通常是因为不像过程地址空间之间的严格隔离,线程的栈区没有严格的隔离机制来爱护,因而如果一个线程能拿到来自另一个线程栈帧上的指针,那么该线程就能够扭转另一个线程的栈区,也就是说这些线程能够任意批改本属于另一个线程栈区中的变量。

这从某种程度上给了程序员极大的便当,但同时,这也会导致极其难以排查到的 bug。

试想一下你的程序运行的好好的,后果某个时刻忽然出问题,定位到出问题代码行后基本就排查不到起因,你当然是排查不到问题起因的,因为你的程序原本就没有任何问题,是他人的问题导致你的函数栈帧数据被写坏从而产生 bug,这样的问题通常很难排查到起因,须要对整体的我的项目代码十分相熟,罕用的一些 debug 工具这时可能曾经没有多大作用了。

说了这么多,那么同学可能会问,一个线程是怎么批改本属于其它线程的数据呢?

接下来咱们用一个代码示例解说一下。

文件

最初,如果程序在运行过程中关上了一些文件,那么过程地址空间中还保留有关上的文件信息,过程关上的文件也能够被所有的线程应用,这也属于线程间的共享资源。对于文件 IO 操作,你能够参考《读取文件时,程序经验了什么?》

One More Thing:TLS

本文就这些了吗?

实际上本篇结尾对于线程公有数据还有一个项没有具体解说,因为再讲下去本篇就撑爆了,实际上本篇解说的曾经足够用了,剩下的这一点仅仅作为补充。

对于线程公有数据还有一项技术,那就是线程部分存储,Thread Local Storage,TLS。

这是什么意思呢?

其实从名字上也能够看出,所谓线程部分存储,是指寄存在该区域中的变量有两个含意:

  • 寄存在该区域中的变量是全局变量,所有线程都能够拜访
  • 尽管看上去所有线程拜访的都是同一个变量,但该全局变量独属于一个线程,一个线程对此变量的批改对其余线程不可见。

说了这么多还是没懂有没有?没关系,接下来看完这两段代码还不懂你来打我。

咱们先来看第一段代码,不必放心,这段代码十分十分的简略:

int a = 1; // 全局变量

void print_a() {cout<<a<<endl;}

void run() {
    ++a;
    print_a();}

void main() {thread t1(run);
    t1.join();

    thread t2(run);
    t2.join();}

怎么样,这段代码足够简略吧,上述代码是用 C ++11 写的,我来解说下这段代码是什么意思。

  • 首先咱们创立了一个全局变量 a,初始值为 1
  • 其次咱们创立了两个线程,每个线程对变量 a 加 1
  • 线程的 join 函数示意该线程运行结束后才持续运行接下来的代码

那么这段代码的运行起来会打印什么呢?

全局变量 a 的初始值为 1,第一个线程加 1 后 a 变为 2,因而会打印 2;第二个线程再次加 1 后 a 变为 3,因而会打印 3,让咱们来看一下运行后果:

2
3

看来咱们剖析的没错,全局变量在两个线程别离加 1 后最终变为 3。

接下来咱们对变量 a 的定义稍作批改,其它代码不做改变:

__thread int a = 1; // 线程部分存储

咱们看到全局变量 a 后面加了一个__thread 关键词用来润饰,也就是说咱们通知编译器把变量 a 放在线程部分存储中,那这会对程序带来哪些扭转呢?

简略运行一下就晓得了:

2
2

和你想的一样吗,有的同学可能会大吃一惊,为什么咱们明明对变量 a 加了两次,但第二次运行为什么还是打印 2 而不是 3 呢?

想一想这是为什么。

原来,这就是线程部分存储的作用所在,线程 t1 对变量 a 的批改不会影响到线程 t2,线程 t1 在将变量 a 加到 1 后变为 2,但对于线程 t2 来说此时变量 a 仍然是 1,因而加 1 后仍然是 2。

因而,线程部分存储能够让你应用一个独属于线程的全局变量。也就是说,尽管该变量能够被所有线程拜访,但该变量在每个线程中都有一个正本,一个线程对扭转量的批改不会影响到其它线程。

总结

怎么样,没想到教科书上一句简略的“线程共享过程资源”背地居然会有这么多的知识点吧,教科书上的常识的确干燥,但,并不简略

心愿本篇能对大家了解过程、线程能有多帮忙。

正文完
 0