有了进程为什么还要有线程

核心提示作者 | 码农的荒岛求生头图 | CSDN 下载自视觉中国进程和线程这两个话题是程序员绕不开的,操作系统提供的这两个抽象概念实在是太重要了。关于进程和线程有一个极其经典的问题,那就是进程和线程的区别是什么?关于这个问题,有的同学可能已经背得

作者|码农的荒岛求生

头部| CSDN下载自视觉中国

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

关于进程和线程有一个极其经典的问题。进程和线程的区别是什么?

关于这个问题,有些同学可能已经烂熟于心了:“进程是操作系统的资源分配单位,线程是调度的基本单位,线程共享进程资源”。

但是你真的理解上面最后一句话吗?线程之间共享哪些进程资源,共享资源是什么意思?资源共享的机制是如何实现的?

如果你没有答案,这篇文章是给你的。

逆向思维

查理·芒格经常这么说:“反过来想,总是反过来想”。

如果你搞不清楚哪些进程资源是线程共享的,不妨反过来想想,哪些资源是线程私有的。

线程私有资源

跑步的本质其实就是函数的执行。一个函数的执行总有一个来源,也就是所谓的入口函数。CPU从入口函数开始执行形成一个执行流,但是我们人为的给这个执行流起了一个名字,这个名字就叫线程。

既然线程运行的本质是函数执行,那么函数执行有什么信息呢?

函数的运行时信息存储在堆栈框架中,堆栈框架中存储了函数的返回值、调用其他函数的参数、函数使用的局部变量以及函数使用的寄存器信息。如图所示,假设函数A调用函数B:

另外,CPU执行的指令的信息存储在一个叫做程序计数器的寄存器中,通过它我们可以知道下一步执行哪条指令。由于操作系统可以在任何时候暂停一个线程的运行,所以我们可以通过保存和恢复程序计数器中的值来知道线程在哪里被暂停,在哪里继续运行。

因为线程运行的本质是函数运行,而函数运行的信息存储在堆栈框架中,所以每个线程都有自己独立私有的堆栈区。

同时,函数需要额外的寄存器来保存一些信息,比如一些局部变量。这些寄存器也是线程私有的,一个线程不可能访问另一个线程的这种寄存器信息。

从上面的讨论中我们知道,到目前为止,线程的函数运行所使用的堆栈区、程序计数器、堆栈指针和寄存器都是线程私有的。

以上信息有一个统一的名称,即线程上下文,thread context。

我们也说过,操作系统在调度线程时需要随时中断线程的运行,线程被挂起后还可以继续运行。操作系统之所以能实现这一点,取决于线程上下文信息。

现在你应该知道哪些线程是私有的了。

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

那还剩下什么?照片上的这些。

其实这就是进程地址空的样子,也就是说线程共享进程地址空中除线程上下文信息以外的所有内容,也就是说线程可以直接读取这些内容。

接下来,我们来分别看一下这几个区域。

代码区

进程地址空中的代码区,这里存放的是什么?有些同学可能从名字就猜到了,没错,这里保存的是我们写的代码,更准确的说是编译好的可执行机器指令。

那么这些机器指令从何而来?答案从可执行文件加载到内存中,可执行程序中的代码区用来初始化进程地址空中的代码区。

线程共享代码区,是指程序中的任何函数都可以放到一个线程中执行,不存在某个函数只能由特定线程执行的情况。

数据区

address 空 room中的数据区是存储所谓全局变量的地方。

什么是全局变量?所谓的全局变量是那些你在函数之外定义的变量,就像C语言中的这个:

char c;//全局变量void func {}

字符C是一个全局变量,它存储在进程地址空之间的数据区中。

在程序员运行的时候,也就是运行时,数据区只有一个全局变量的实例,所有线程都可以访问全局变量。

值得注意的是,C语言中有一种特殊的“全局变量”,即用静态关键字修饰的变量,像这样:

voidfunc { staticint a = 10}

注意,虽然在函数中定义了变量A,但变量A仍然具有全局变量的特征,也就是说,变量A被放在进程地址空之间的数据区。即使函数执行后变量仍然存在,普通的局部变量也会在函数调用结束时随函数栈帧一起被回收,但这里的变量A不会被回收,因为它被放在了数据区。

这样的变量对每个线程都是可见的,这意味着每个线程都可以访问该变量。

堆区域

堆区是程序员熟悉的,我们在C/C++中使用malloc或new的数据都存储在这个区域。显然,只要知道变量的地址,也就是指针,任何线程都可以访问指针所指向的数据,所以堆区也是线程共享的资源,属于进程。

堆栈区

哦,等等!你刚才不是说堆栈区是线程的私有资源吗?为什么现在说栈区?

的确,从线程的抽象概念来看,堆栈区域是线程私有的。但从实际实现来看,并没有严格遵守堆栈区属于线程私有的规则。这句话是什么意思?

一般来说,堆栈区是线程私有的,既然有通常的时候,就有不寻常的时候。

通常并不是因为,与进程地址空之间的严格隔离不同,线程的堆栈区域并没有受到严格隔离机制的保护。因此,如果一个线程可以从另一个线程的堆栈帧中获取指针,那么该线程就可以更改另一个线程的堆栈区域,这意味着这些线程可以任意修改属于另一个线程的堆栈区域的变量。

这在一定程度上给程序员带来了极大的便利,但同时也导致了极其难以察觉的bug。

想象一下,你的程序运行得很好,突然在某个时刻出了问题。定位出了问题的代码行之后也找不出原因。当然,你找不出问题的原因,因为你的程序没有问题。是别人的问题导致你的函数栈帧数据被写下来,产生bug。通常很难找出这类问题的原因,你需要非常熟悉整个项目代码。一些常用的调试工具此时可能没有太大的作用。

说到这里,同学们可能会问,一个线程如何修改属于其他线程的数据?

接下来,我们用一个代码示例来解释一下。

修改线程私有数据

别担心,下面的代码很简单:

voidthread { int * p = var* p = 2;} int main { int a = 1;pthread _ t tidpthread _ create a);return0}

这个代码是什么意思?

首先,我们在主线程的堆栈区定义了一个局部变量,即int a= 1。现在我们知道局部变量A属于主线程的私有数据,但是后来我们又创建了另一个线程。

在新创建的线程中,我们将变量A的地址作为参数传递给新创建的线程,然后我看了一下线程函数。

在新创建的线程中,我们获取变量A的指针,然后修改为2,就是这行代码。在新创建的线程中,我们修改了属于主线程的私有数据。

现在你应该明白了,虽然堆栈区是一个线程的私有数据,但是一个线程的堆栈区对其他线程是可见的,因为没有给它添加保护机制,这意味着我们可以修改属于任何线程的堆栈区。

我们上面说过,这给程序员带来了极大的便利,但也带来了无尽的烦恼。想象一下上面的代码。如果真的是项目需要,这样写代码无可厚非。但是,如果上面提到的新创建的线程因为bug修改了属于其他线程的私有数据,就很难定位问题,因为bug可能离暴露的代码很远,这类问题通常很难排查。

动态链接库

address 空房间里其实除了上面讨论的内容还有其他内容。还有什么?

这就要从可执行程序说起了。

什么是可执行程序?在Windows中是我们熟悉的exe文件,在Linux中是ELF文件。这些可以由操作系统直接运行的程序就是我们所说的可执行程序。

那么可执行程序是怎么产生的呢?

有的同学可能会说,废话,不就是编译器生成的吗?

其实这个答案只对了一半。

假设我们的项目很简单,只有几个源代码文件,编译器如何将这些源代码文件转换成最终的可执行程序?

原来编译器把可执行程序翻译成机器指令之后,还有一个重要的步骤,就是链接。链接后,生成可执行程序。

完成链接过程的是链接器。

链接器可以有两种链接方式,即静态链接和动态链接。

静态链接是指把所有的机器指令打包成可执行程序,而动态链接是指我们不把动态链接的部分打包成可执行程序,而是在可执行程序运行后,在内存中寻找代码的动态链接部分,这就是所谓的静态链接和动态链接。

动态链接的一个明显的好处就是可执行程序的大小会非常小,就像我们在Windows下看一个exe文件,很可能这个exe就是动态链接生成的。

动态链接部分生成的库就是大家熟悉的动态链接库,Windows中以DLL结尾,Linux中也是如此。

说了这么多,这跟线程共享资源有什么关系?

如果一个程序是动态链接生成的,那么它的一部分地址空中就包含了动态链接库,否则程序就不会运行,而这部分地址空也是所有线程共享的。

也就是说,进程中的所有线程都可以使用动态链接库中的代码。

文件

最后,如果在程序运行过程中打开了一些文件,打开的文件的信息存储在进程地址空中,进程打开的文件也可以被所有线程使用,这也属于线程间的共享资源。

坦克激光瞄准镜(Tank Laser-Sight的缩写)

这篇文章就这些吗?

其实关于线程私有数据还有一点没有详细解释,因为再继续讲下去这篇文章就满了,这篇文章已经解释的部分就够了。剩下的只是补充,也就是选课的部分。如果你对它不感兴趣,完全可以跳过,没问题。

还有一个关于线程私有数据的技术,就是线程本地存储,TLS。

这是什么意思?

其实从名字就可以看出,所谓的线程本地存储,就是存储在这个区域的变量有两种含义:

存储在这个区域的变量是全局变量,所有线程都可以访问。虽然看起来所有线程访问的是同一个变量,但是全局变量只属于一个线程,一个线程对这个变量的修改是其他线程看不到的。说了这么多,你还不明白吗?没关系。看完这两个代码,你就可以打我了。

我们先来看第一段代码。别担心,这段代码非常非常简单:

int a = 1; // 全局变量voidprint_a { cout

 
友情链接
鄂ICP备19019357号-22