# 4.1 线程的简单介绍与同步方法

### 线程介绍

这里的线程主要为 POSIX 线程，即 Pthreads，由 Native POSIX Threads Library（NPTL）实现。 线程是允许应用程序并发执行多个任务的一种机制，一个进程可以包含多个线程，线程可以共享同一份全局内存区域。 使用多进程实现并行有以下不足：

* 进程间的信息难以共享，必须采用进程间通信（IPC）方式
* 调用 fork 创建进程的代价比较高

线程完美的解决了上面两个问题，线程还可共享：

* 进程 ID，父进程 ID
* 进程组 ID 与会话 ID
* 打开的文件描述符
* 信号处置

各个线程的独有属性有：

* 线程 ID
* 信号掩码
* 线程特有数据
* erron 变量，是一个宏
* 栈

后续 Pthreads 系列函数均以 0 表示成功，返回一个正值表示失败，这个正值与传统 Unix 的 errno 的值含义一样。在编译调用了 Pthreads 函数的程序需要添加 `-lpthread`以链接此库。

#### 创建线程

程序刚启动时，只包含一条线程，称为主线程。

```c
#include <pthread.h>
/*
@brief	创建线程
@param	thread 	线程 ID，通过该标识可以引用该进程
@param	attr	指定了新线程的各种属性，NULL 则使用默认属性
@param	start	新线程将执行 start 函数
@param	arg	为传递给 start 的参数
*/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start)(void *)， void *arg);
```

创建新线程后，系统会优先调度哪一个是不确定的，如果对执行顺序有明确要求可采取一些同步技术（见后续的线程同步小节）。

#### 终止线程

可采用以下方式终止线程：

* 线程 start 函数执行到 return 语句
* 线程调用 pthread\_exit
* 调用 pthread\_cancel 取消线程
* 任意线程调用了 exit，或者主线程（main）执行了 return

```c
#include <pthread.h>
// 终止线程，retval 指定了线程的返回值，其指向的内容不应该分配在线程栈中
// 因为线程终止后，线程栈内的内容不一定有效
void pthread_exit(void *retval);
```

#### 线程 ID

线程 ID 是线程的唯一标识，pthread\_create 可返回线程 ID，线程也可获取自己的线程 ID：

```c
#include <pthread.h>
pthread_t pthread_self(void);
```

不能直接使用 == 运算符判断两个线程 ID 是否相等，因为其底层实现可能是无符号长整型、指针、结构：

```c
#include <pthread.h>
// 判断线程 ID 是否相同
int pthread_equal(pthread_t t1, pthread_t t2);
```

但 NPTL 中，它实际上是一个经强制转化为无符号长整形的指针。 在 Linux 中线程 ID 在所有进程中都是唯一的，不过其它 Unix 实现则不一定。线程 ID 可复用，当某个线程终止后，其 ID 可被后续新线程复用。

#### 连接已终止的线程

类似进程中的 wait 系列函数：

```c
#include <pthread.h>
// 等待 thread 线程终止，并将线程返回值拷贝到 retval 处，若其为非空指针
int pthread_join(pthread_t thread, void **retval);
```

与进程类似，若线程结束后没有连接，则会产生僵尸线程，除非手动将其分离（后续介绍）。如向 pthread\_join 传入一个已经连接过的线程将会导致无法预知的行为，因为线程 ID 可能会被复用。 线程的连接与进程中的 wait 有以下不同：

* 线程之间的关系是对等的，不像进程存在父子关系。进程中的任意线程均可以调用 pthread\_join 与其他线程连接起来。
* 不能以非阻塞方式连接，也不能连接任意线程（如别的进程中的线程）。

#### 线程分离

线程默认清空下是可连接的，但也可指定其为分离状态。分离状态的线程在终止后，系统会自动清理并移除。当不关心线程的返回状态时，这十分有用。

```c
#include <pthread.h>
// 调用 pthread_detach(pthread_self)); 可自行分离
int pthread_detach(pthread_t thread);
```

一旦线程处理分离状态，则不可被连接，也不能重返可连接状态。在其他线程调用了 exit 或者主线程 return 后，分离的线程仍然会被终止。

#### 线程与进程

线程有以下优点：

* 线程间数据共享十分简单
* 创建线程很快

相比与进程有以下缺点：

* 多线程编程时，需要确保调用线程安全函数，或以线程安全的方式调用函数（后续介绍）
* 因为共享共一个全局空间，某个线程的 bug 可能会影响到全局
* 线程共用宿主进程的虚拟地址空间，分配大量线程时，则会出现问题
* 线程中处理信号十分麻烦，在多线程程序中应避免使用信号

### 线程同步

线程同步旨在同步对共享资源的使用，可通过互斥量和条件变量实现。

#### 互斥量

线程可以直接访问全局变量，但必须确保多个线程不会同时修改同一变量。这部分全局变量称之为临界区（共享资源），操作临界区的代码必须是原子的，某线程访问临界区时，不应该被其他线程中断。 互斥量有两种状态：已锁定和未锁定。任何时候只有一个线程可以锁定该互斥量，试图对一个已经上锁的互斥量加锁可能会阻塞进程或者返回失败。 一旦线程锁定某一互斥量，便只有该线程可以解锁。一般情况下，对不同共享资源会使用不同的互斥量，每个线程在访问同一资源时，一般遵循：

1. 针对该共享资源锁定互斥量
2. 访问共享资源
3. 解锁

互斥量类型为 `pthread_mutex_t`，在使用之前必须对其初始化：

```c
#include <pthread.h>
// 静态初始化
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
/*
@brief	动态初始化，attr 为该互斥量的各种属性值，NULL 则使用默认属性
@param	attr 包括了互斥量的类型：
        - PTHREAD_MUTEX_NORMAL		此类型互斥量不具有死锁检测功能
        - PTHREAD_MUTEX_ERRORCHECK	这类互斥量的所有操作均会进行错误检查
        				不过效率会差一些，可用作 Debug
        - PTHREAD_MUTEX_RECURSIVE	递归互斥量维护一个锁计数器，每次锁定都会将计数器值加 1
                                        解锁操作递减计数器，当计数器为 0 时才会释放
        
*/
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
// 动态初始化的互斥量需要手动销毁
int pthread_mutex_destory(pthread_mutex_t *mutex);

```

加锁和解锁互斥量：

```c
#include <pthread.h>
// 尝试给互斥量加锁，若互斥量已被锁定，则会一直阻塞，直到该互斥量被解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 对为锁定的互斥量解锁，或解锁其他线程锁定互斥量均会返回错误
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 不会阻塞线程，信号量已经被锁定会失败并返回 EBUSY 错误
int pthread_mutex_trylock(pthread_mutex_t *mutex);
```

当线程需要同时访问多个不同资源时，这些资源由互斥量管理，则可能会发生死锁。例如：

* 线程 A 锁定了 mutex1
* 线程 B 锁定了 mutex2
* 线程 A 尝试锁定 mutex2， 线程 B 尝试锁定 mutex1
* 死锁发生

要避免此类死锁问题，最简单的办法就是定义互斥量的层级关系。当多个线程对一组互斥量操作时，总是以相同的顺序对互斥量进行锁定。 或者先使用 pthread\_mutex\_lock 锁定第一个互斥量，后续互斥量使用 pthread\_mutex\_trylock 尝试锁定，如果失败，则解锁之前锁定的互斥量。这种方法效率会低一些，但比较灵活。

#### 条件变量

互斥量防止多个线程同时访问同一共享资源，条件变量允许线程阻塞，直到某个线程就某个共享资源的状态变化通知其他线程。 采用条件变量可以允许一个线程休眠，直至收到另一线程的通知。条件变量一般结合互斥量使用，就互斥量状态改变发出通知。 条件变量也有静态分配和动态分配两种方式：

```c
#include <pthread.h>
// 静态分配
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 动态分配，attr 为该条件变量的一些属性，NULL 使用默认属性
int pthred_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
// 动态分配的条件变量需要手动销毁
int pthread_cond_destory(pthread_cond_t *cond);
```

条件变量的主要操作是发送信号（通知其他线程，此信号并不是系统的那个信号）和等待。等待操作是指收到一个通知前一直处于阻塞状态。

```c
#include <pthread.h>

// signal 保证唤醒至少一条遭到阻塞的线程
int pthread_cond_signal(pthread_cond_t *cond);
// broadcast 唤醒所有遭阻塞的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 阻塞此线程直到收到条件变量 cond 的通知
int pthread_cond_wait(pthread_cond_t *cond, pthread_t *mutex);
// 使用参数 abstime 设置等待上限，而不会一直阻塞
// 到时且无状态变化通知则返回 ETIMEOUT 错误
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_t *mutex,
                          const struct timespec *abstime);
```

条件变量并不保存状态信息，发送信号通知其他线程时，若无线程在等待该条件变量，这个信号则会被忽略。

#### pthread\_cond\_wait

pthread\_cond\_wait 会阻塞线程直到收到条件变量，其内部会执行如下步骤：

1. 解锁互斥量 mutex，此互斥量与控制目标资源访问的互斥量是同一个
2. 阻塞调用线程，直到另一线程就此条件变量发出信号
3. 重新锁定 mutex

```c
pthread_mutex_lock(&mtx);
while(/* 检查共享资源的状态是否是想要的 */)
    pthread_cond_wait(&cond, &mtx);
pthread_mutex_unlock(&mtx);
```

其总与访问共享资源的同一个互斥量绑定，共享资源的互斥量与条件变量一般有以下形式：

1. 线程锁定互斥量以访问共享资源
2. 检查共享资源状态
3. 如果共享资源并没有处于预期状态，则应该解锁互斥量（以便其他线程可以访问），并阻塞
4. 线程因为条件变量被唤醒后，应立即加锁，因为一般线程会马上访问共享变量

函数 pthread\_cond\_wait 会自动执行 3 4 步的解锁和加锁，且解锁与陷入阻塞为一个原子操作，其他线程不会在此线程陷入阻塞前获取到此互斥量。 上面的代码中并没有用 if 语句判断 pthread\_cond\_wait 控制的条件的状态，而是采用 while 不断检查资源状态，因为从 pthread\_cond\_wait 中返回时不能对判断条件的状态做任何假设，因为可能会存在虚假唤醒的情况。

#### 例子：生产者-消费者

```c
/*
@Brief 生产者消费者问题
*/
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int produced_num = 0;

// 生产者线程
void* producer(void* arg) {
    int idx = (int)arg;
    while(1) {
        pthread_mutex_lock(&mtx);
        ++produced_num;
        printf("thread:%d produced, num: %d\n", idx, produced_num);
        pthread_mutex_unlock(&mtx);
        sleep(1);
        // 发送信号表示已经生产
        pthread_cond_signal(&cond);
    }
    return NULL;
}

int main(int argc, char** argv) {
    int thread_num = 3; // 线程数量
    // 创建线程
    for(int i = 0;i < thread_num;++i) {
        pthread_t t_id;
        int s = pthread_create(&t_id, NULL, producer, (void*)i);
        pthread_detach(t_id);
    }
    // 主线程为消费者
    while(1) {
        pthread_mutex_lock(&mtx);
        // 为 0 则等待生产者生产
        while(produced_num == 0) {
            int s = pthread_cond_wait(&cond, &mtx);
            if(s != 0) {
                exit(EXIT_FAILURE);
            }
        }
        while(produced_num > 0) {
            --produced_num;
            printf("consume, now num:%d\n", produced_num);
        }
        pthread_mutex_unlock(&mtx);
    }
    return 0;
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://linuxprogramming.weijun-lin.top/4.-xian-cheng/4.1-xian-cheng-de-jian-dan-jie-shao-yu-tong-bu-fang-fa.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
