- 出版者的话
- 中文版序一
- 中文版序二
- 译者序
- 前言
- 关于作者
- 第 1 章:计算机系统漫游
- 第 2 章:信息的表示和处理
- 第 3 章:程序的机器级表示
- 第 4 章:处理器体系结构
- 第 5 章:优化程序性能
- 第 6 章:存储器层次结构
- 第 7 章:链接
- 第 8 章:异常控制流
- 第 9 章:虚拟内存
- 第 10 章:系统级 I/O
- 第 11 章:网络编程
- 第 12 章:并发编程
- 附录 A:错误处理
- 实验 1:Data Lab
- 实验 3:Attack Lab
- 实验 4:Architechture Lab
- 实验 5:Cache Lab
- 实验 6:Performance Lab
- 实验 7:Shell Lab
- 实验 8:Malloc Lab
- 实验 9:Proxy Lab
12.4 多线程程序中的共享变量
从程序员的角度来看,线程很有吸引力的一个方面是多个线程很容易共享相同的程序变量。然而,这种共享也是很棘手的。为了编写正确的多线程程序,我们必须对所谓的共享以及它是如何工作的有很清楚的了解。
为了理解 C 程序中的一个变量是否是共享的,有一些基本的问题要解答:
- 线程的基础内存模型是什么?
- 根据这个模型,变量实例是如何映射到内存的?
- 最后,有多少线程引用这些实例?一个变量是共享的,当且仅当多个线程引用这个变量的某个实例。
为了让我们对共享的讨论具体化,我们将使用图 12-15 中的程序作为运行示例。尽管有些人为的痕迹,但是它仍然值得研究,因为它说明了关于共享的许多细微之处。示例程序由一个创建了两个对等线程的主线程组成。主线程传递一个唯一的 ID 给每个对等线程,每个对等线程利用这个 ID 输出一条个性化的信息,以及调用该线程例程的总次数。
#include "csapp.h"
#define N 2
void *thread(void *vargp);
char **ptr; /* Global variable */
int main()
{
int i;
pthread_t tid;
char *msgs[N] = {
"Hello from foo",
"Hello from bar"
};
ptr = msgs;
for (i = 0; i < N; i++)
Pthread_create(&tid, NULL, thread, (void *)i);
Pthread_exit(NULL);
}
void *thread(void *vargp)
{
int myid = (int)vargp;
static int cnt = 0;
printf("[%d]: %s (cnt=%d)\n", myid, ptr[myid], ++cnt);
return NULL;
}
图 12-15 说明共享不同方面的示例程序
12.4.1 线程内存模型
一组并发线程运行在一个进程的上下文中。每个线程都有它自己独立的线程上下文,包括线程 ID、栈、栈指针、程序计数器、条件码和通用目的寄存器值。每个线程和其他线程一起共享进程上下文的剩余部分。这包括整个用户虚拟地址空间,它是由只读文本(代码)、读/写数据、堆以及所有的共享库代码和数据区域组成的。线程也共享相同的打开文件的集合。
从实际操作的角度来说,让一个线程去读或写另一个线程的寄存器值是不可能的。另一方面,任何线程都可以访问共享虚拟内存的任意位置。如果某个线程修改了一个内存位置,那么其他每个线程最终都能在它读这个位置时发现这个变化。因此,寄存器是从不共享的,而虚拟内存总是共享的。
各自独立的线程栈的内存模型不是那么整齐清楚的。这些栈被保存在虚拟地址空间的栈区域中,并且通常是被相应的线程独立地访问的。我们说通常而不是总是,是因为不同的线程栈是不对其他线程设防的。所以,如果一个线程以某种方式得到一个指向其他线程栈的指针,那么它就可以读写这个栈的任何部分。示例程序在第 26 行展示了这一点,其中对等线程直接通过全局变量 ptr 间接引用主线程的栈的内容。
12.4.2 将变量映射到内存
多线程的 C 程序中变量根据它们的存储类型被映射到虚拟内存:
- 全局变量。全局变量是定义在函数之外的变量。在运行时,虚拟内存的读/写区域只包含每个全局变量的一个实例,任何线程都可以引用。例如,第 5 行声明的全局变量 ptr 在虚拟内存的读/写区域中有一个运行时实例。当一个变量只有一个实例时,我们只用变量名(在这里就是 ptr)来表示这个实例。
- 本地自动变量。本地自动变量就是定义在函数内部但是没有 static 属性的变量。在运行时,每个线程的栈都包含它自己的所有本地自动变量的实例。即使多个线程执行同一个线程例程时也是如此。例如,有一个本地变量 tid 的实例,它保存在主线程的栈中。我们用 tid.m 来表示这个实例。再来看一个例子,本地变量 myid 有两个实例,一个在对等线程。的栈内,另一个在对等线程 1 的栈内。我们将这两个实例分别表示为 myid.p0 和 myid.p1。
- 本地静态变量。本地静态变量是定义在函数内部并有 static 属性的变量。和全局变量一样,虚拟内存的读/写区域只包含在程序中声明的每个本地静态变量的一个实例。例如,即使示例程序中的每个对等线程都在第 25 行声明了 cnt,在运行时,虚拟内存的读/写区域中也只有一个 cnt 的实例。每个对等线程都读和写这个实例。
12.4.3 共享变量
我们说一个变量 v 是共享的,当且仅当它的一个实例被一个以上的线程引用。例如,示例程序中的变量 cnt 就是共享的,因为它只有一个运行时实例,并且这个实例被两个对等线程引用。在另一方面,myid 不是共享的,因为它的两个实例中每一个都只被一个线程引用。然而,认识到像 msgs 这样的本地自动变量也能被共享是很重要的。
练习题 12.6
A. 利用 12.4 节中的分析,为图 12-15 中的示例程序在下表的每个条目中填写“是”或者“否”。在第一列中,符号 v.t 表示变量 v 的一个实例,它驻留在线程 t 的本地栈中,其中 t 要么是 m(主线程),要么是 p0(对等线程 0)或者 p1(对等线程 1)。
变量实例 | 主线程引用的? | 对等线程 0 引用的? | 对等线程 1 引用的? |
---|---|---|---|
ptr | |||
cnt | |||
i.m | |||
msgs.m | |||
myid.p0 | |||
myid.p1 |
B. 根据 A 部分的分析,变量 ptr、cnt、i、msgs 和 myid 哪些是共享的?
这里的主要的思想是,栈变量是私有的,而全局和静态变量是共享的。诸如 cnt 这样的静态变量有点小麻烦,因为共享是限制在它们的函数范围内的一一在这个例子中,就是线程例程。
A. 下面就是这张表:
变量实例 | 被主线程引用? | 被对等线程 0 引用? | 被对等线程 1 引用? |
---|---|---|---|
ptr | 是 | 是 | 是 |
cnt | 否 | 是 | 是 |
i.m | 是 | 否 | 否 |
msgs.m | 是 | 是 | 是 |
myid.p0 | 否 | 是 | 否 |
myid.p1 | 否 | 否 | 是 |
说明:
- ptr:一个被主线程写和被对等线程读的全局变量。
- cnt:一个静态变量,在内存中只有一个实例,被两个对等线程读和写。
- i.m:一个存储在主线程栈中的本地自动变量。虽然它的值被传递给对等线程,但是对等线程也绝不会在栈中引用它,因此它不是共享的。
- msgs.m:一个存储在主线程栈中的本地自动变量,被两个对等线程通过 ptr 间接地引用。
- myid.0 和 myid.1;—个本地自动变量的实例,分别驻留在对等线程 0 和线程 1 的栈中。
B. 变量 ptr、ent 和 msgs 被多于一个线程引用,因此它们是共享的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论