多线程中的共享变量
00 前言
相比多进程并发,多线程一个好处就是:多线程很容易共享变量。然而,这种共享并不是很松,处理起来同样很棘手。为了更好地编写多线程程序,需要对所谓的共享和它们如何工作有个很清楚的了解。
01 线程存储器模型
线程有自己独立的线程上下文,包括线程 ID、栈、栈指针、程序计数器、条件码和通用目的寄存器的值。剩余的全部,也就是整个用户虚拟地址空间,包括代码、数据、堆以及共享库代码和数据区域,还要记得,线程也共享打开文件的文件集合。
从实际操作看,让一个线程去读写另一个线程中的寄存器是不可能的。因此,线程之间,寄存器从来不共享,而虚拟存储器总是共享的。
02 变量和存储器之间的映射关系
- 全局变量 。进程内只有一个变量的实例,所有线程共享。
- 本地自动变量。定义在函数内部,没有使用 static 属性的变量。运行时,每个线程的栈都包含它自己本地自动变量的实例,即使多个线程执行的是同一个例程。
- 本地静态变量。定义在函数内部,并使用 static 属性的变量。和全局变量一致,虚拟存储器读写区域也只包含每个本地静态变量的一个实例。每个对等线程也只有一个实例。
03 共享变量
共享是指,当且仅当变量实例被一个以上的线程引用。
对等线程内部,本地静态变量可以共享,本地自动变量不可以。同时,主函数之外的全局变量可以共享,主函数内,主线程的本地自动变量也是可以共享的。
04 用信号量同步线程
共享变量是十分方便的,但同时也引入了同步错误。
例如,在一个例程中,有一个循环,循环次数确定为 10000,循环的内容很简单,只是在每次循环时,将共享的全局变量实例的值增加 1 。我们通过这个例程建立两个对等线程,分别执行。等待它们各自运行结束时,检查全局变量的值,会发现其增加量并不是 10000 X 2 。
这里的问题在于,C 语言里的循环语句中的一个增值操作,在汇编语言中,实际上有一系列步骤。所谓并发执行,在单核处理器上,往往是两个循环体的汇编层面上的某种合序,可以认为是一种交叉。交叉之后,就不再一定是我们想要的并行,因为粒度完全不一致了,已经混合在一起。混合之后的顺序,有的对,有的不对。
再加上:一般而言,没有办法干预操作系统调度线程并发时的执行顺序一定是符合自己意图的那一个。
而处理这个问题,需要进度图:将 n 个并发线程的执行模型化为一条 n 维笛卡尔空间中的轨迹线,构成轨迹的坐标是每个线程下的指令状态序列。
每个坐标就是一个状态,状态之间的变化,代表程序的执行,称为转换。
合法的转换是有限制的:
- 不能下行或左行,只能上行或右行。
- 两个线程内的指令不可能在同一个时刻完成,因此对角转换也是不允许的。
- 操作共享变量的指令构成一个子状态空间,这是一个不安全区。不同线程不能同时执行不安全区内的指令,这意味着,轨迹线不能进入不安全区。
那问题来了,怎么控制轨迹线呢?通过信号量:
- 信号量 S 是一种特殊类型的变量,为非负整数值
- P(s) 操作。如果 s 非零,P 将 s 减 1,并且立即返回。如果 s 为零,那么就挂起线程,直到 s 变为非零,而一个 V 操作会重启这个线程。在重启之后,P 操作将 s 减 1,并将控制返回给调用者。
- V(s) 操作。V 操作将 s 加 1。如果有任何线程阻塞在 P 操作等待 s 变为非零,那么 V 将重启这些线程中的一个,然后将该线程减 1,完成它的 P 操作。
- P 中检测 s 是否非零和减 1 之间不能中断,V 中 同理。
- P 和 V 可以确保正确初始化了的信号量不会有负值。
待续。。。
不知是该恭喜,还是该怎样,总之阅读到该文的,你是第 人。每一次刷新,都是不同的自己。