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

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

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

1. 线程的基础内存模型是什么？
2. 根据这个模型，变量实例是如何映射到内存的？
3. 最后，有多少线程引用这些实例？一个变量是**共享的**，当且仅当多个线程引用这个变量的某个实例。

为了让我们对共享的讨论具体化，我们将使用图 12-15 中的程序作为运行示例。尽管有些人为的痕迹，但是它仍然值得研究，因为它说明了关于共享的许多细微之处。示例程序由一个创建了两个对等线程的主线程组成。主线程传递一个唯一的 ID 给每个对等线程，每个对等线程利用这个 ID 输出一条个性化的信息，以及调用该线程例程的总次数。

{% tabs %}
{% tab title="code/conc/sharing.c" %}

```c
#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;
}
```

{% endtab %}
{% endtabs %}

> 图 12-15 说明共享不同方面的示例程序

## 12.4.1 线程内存模型

一组并发线程运行在一个进程的上下文中。每个线程都有它自己独立的线程上下文，包括线程 ID、栈、栈指针、程序计数器、条件码和通用目的寄存器值。每个线程和其他线程一起共享进程上下文的剩余部分。这包括整个用户虚拟地址空间，它是由只读文本（代码）、读/写数据、堆以及所有的共享库代码和数据区域组成的。线程也共享相同的打开文件的集合。

从实际操作的角度来说，让一个线程去读或写另一个线程的寄存器值是不可能的。另一方面，任何线程都可以访问共享虚拟内存的任意位置。如果某个线程修改了一个内存位置，那么其他每个线程最终都能在它读这个位置时发现这个变化。因此，寄存器是从不共享的，而虚拟内存总是共享的。

各自独立的线程栈的内存模型不是那么整齐清楚的。这些栈被保存在虚拟地址空间的栈区域中，并且通常是被相应的线程独立地访问的。我们说通常而不是总是，是因为不同的线程栈是不对其他线程设防的。所以，如果一个线程以某种方式得到一个指向其他线程栈的指针，那么它就可以读写这个栈的任何部分。示例程序在第 26 行展示了这一点，其中对等线程直接通过全局变量 ptr 间接引用主线程的栈的内容。

## 12.4.2 将变量映射到内存

多线程的 C 程序中变量根据它们的存储类型被映射到虚拟内存：

* **全局变量。**&#x5168;局变量是定义在函数之外的变量。在运行时，虚拟内存的读/写区域只包含每个全局变量的一个实例，任何线程都可以引用。例如，第 5 行声明的全局变量 ptr 在虚拟内存的读/写区域中有一个运行时实例。当一个变量只有一个实例时，我们只用变量名（在这里就是 ptr）来表示这个实例。
* **本地自动变量。**&#x672C;地自动变量就是定义在函数内部但是没有 static 属性的变量。在运行时，每个线程的栈都包含它自己的所有本地自动变量的实例。即使多个线程执行同一个线程例程时也是如此。例如，有一个本地变量 tid 的实例，它保存在主线程的栈中。我们用 **tid.m** 来表示这个实例。再来看一个例子，本地变量 myid 有两个实例，一个在对等线程。的栈内，另一个在对等线程 1 的栈内。我们将这两个实例分别表示为 **myid.p0** 和 **myid.p1**。
* **本地静态变量。**&#x672C;地静态变量是定义在函数内部并有 static 属性的变量。和全局变量一样，虚拟内存的读/写区域只包含在程序中声明的每个本地静态变量的一个实例。例如，即使示例程序中的每个对等线程都在第 25 行声明了 cnt，在运行时，虚拟内存的读/写区域中也只有一个 cnt 的实例。每个对等线程都读和写这个实例。

## 12.4.3 共享变量

我们说一个变量 v 是共享的，当且仅当它的一个实例被一个以上的线程引用。例如，示例程序中的变量 cnt 就是共享的，因为它只有一个运行时实例，并且这个实例被两个对等线程引用。在另一方面，myid 不是共享的，因为它的两个实例中每一个都只被一个线程引用。然而，认识到像 msgs 这样的本地自动变量也能被共享是很重要的。

## 练习题 12.6

{% tabs %}
{% tab title="练习题 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 哪些是共享的？
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="答案" %}
这里的主要的思想是，栈变量是私有的，而全局和静态变量是共享的。诸如 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 被多于一个线程引用，因此它们是共享的。
{% endtab %}
{% endtabs %}
