12.4 多线程程序中的共享变量

从程序员的角度来看,线程很有吸引力的一个方面是多个线程很容易共享相同的程序变量。然而,这种共享也是很棘手的。为了编写正确的多线程程序,我们必须对所谓的共享以及它是如何工作的有很清楚的了解。

为了理解 C 程序中的一个变量是否是共享的,有一些基本的问题要解答:

  1. 线程的基础内存模型是什么?

  2. 根据这个模型,变量实例是如何映射到内存的?

  3. 最后,有多少线程引用这些实例?一个变量是共享的,当且仅当多个线程引用这个变量的某个实例。

为了让我们对共享的讨论具体化,我们将使用图 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.p0myid.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 被多于一个线程引用,因此它们是共享的。

最后更新于