编写线程安全的代码

进程的内存空间布局

进程的内存布局.png

栈区:函数的运行时栈

共享内存映射区:用于存储进程的映射文件、共享内存、动态链接库等内容。也可以用于映射设备文件或其他资源。

堆区:malloc 分配的内存区域

数据区:保存全局变量和静态变量

代码区:代码编译后形成的机器指令

注意:数据段又分为已初始化的数据段和未初始化的数据段,即数据段存储程序中已初始化和未初始化的全局变量及静态变量。

线程的私有资源

栈:线程运行函数的栈区,用于存储该线程的局部变量、函数参数、返回地址等信息

寄存器:每个线程在CPU上执行时都会使用寄存器存储当前的执行状态,例如程序计数器(PC)

线程局部存储:线程局部存储允许线程拥有自己独立的全局变量副本,而这些变量在不同线程之间不会共享

线程间的共享资源

线程的私有资源e.png

一个线程就是执行一个函数,进程中的线程就是对应着进程内存布局中的栈区,除去栈区是每个线程独有的资源,其余区域就属于多个线程共享的资源,下面逐一分析。

代码区

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

由于代码区是只读的,因此这个区域在多个线程之间不存在线程安全问题

数据区

存放静态变量和全局变量,多个线程共享,可读可写,存在线程安全问题

堆区

存放 malloc 申请的内存,多个线程只要拿到执行这块内存的指针,就可以操作这块内存数据(可读可写),存在线程安全问题

动态链接库与文件

动态库的代码和数据都放在空闲区域,即共享内存映射区。这种第三方库的问题,就很难明确说是否线程安全,还得看调用者和被调用者代码的情况。

栈区

明明我们前面已经谈及 栈区是各个线程私有的,为什么还会纳入共享资源呢?

不同的进程的地址空间相互隔离,虚拟内存系统确保这点,但是不同的线程的栈区没有严格的隔离机制来保护。

因此如果一个线程能拿到来自另一个线程栈帧上的指针,那么该线程就可以改变另一个线程的栈区,也就是说这些线程可以任意修改本属于另一个线程栈区中的变量。

1
2
3
4
5
6
7
8
9
10
11
12
void thread(void* var) {
int* p = (int*)var;
*p = 2;
}

int main() {
int a = 1;
pthread_t tid;

pthread_create(&tid, NULL, thread, (void*)&a); // 子线程 修改 主线程的局部变量 a
return 0;
}

一个线程修改另一个线程的数据,真是相当危险,且难以排查。你以为是你写的函数有问题,结果是其他函数修改你的数据,这让人怎么去查?

如果你不足够熟悉涉及到的代码,debug 可是不那么容易弄出来的。

编写线程安全的代码

只使用线程的私有资源:函数内定义的局部变量

函数的局部参数:参数传递有值传递和引用传递,值传递是线程安全,引用传递不是线程安全

线程局部存储:虽然多线程共享,但是各自有各自的副本,确保是线程安全

函数的返回值:返回值没有问题,但是返回指针会存在线程安全问题

原子操作和同步互斥:原子操作是线程安全,同步和互斥是常见的保证线程安全的方法