# 4.2 线程安全、线程取消

### 线程安全

#### 线程安全函数

若函数可同时供多个线程安全调用，则称之为线程安全函数。如以下函数：

```c
static int glob = 0;
static void incr(int loops) {
    int loc, j;
    for(j = 0;j < loops;j++) {
        loc = glob;
        loc++;
        glob = loc;
    }
}
```

一般导致线程不安全的典型原因是：使用了所有线程之间共享的全局或静态变量。可通过互斥量与共享变量关联起来，已保证线程安全。

#### 一次性初始化

一次性初始化是指，无论创建多少线程，初始化动作只发生一次：

```c
#include <pthread.h>

/*
@brief	无论调用过 pthread_once 多少次，都只会调用 init 一次
@param	once_control	一般初始化为：pthread_once_t once_var = PTHREAD_ONCE_INIT;
*/
int pthread_once(pthread_once_t *once_control, void (*init)(void));
```

此函数经常与线程特有数据结合使用。

#### 线程特有数据

线程特有数据为每个线程提供一份独有的数据副本。一般步骤如下：

1. 函数创建一个键（key），用以区分不同函数使用的线程特有函数，可通过 `pthread_key_create` 创建，且只需要创建一次，所以需要配合 `pthread_once` 使用。
2. `pthread_key_create`还允许调用者指定一个自定义解构函数，在线程终止时释放该线程的特有数据块
3. 使用 malloc 创建特有数据
4. 上一步分配的数据地址可通过 `pthread_setspecific`和 `pthread_getspecific`设置和获取。一般先获取数据地址，若为 NULL 说明还没有分配，然后使用 `pthread_setspecific`设置数据地址。

```c
#include <pthread.h>

// 设置 key 以及解构函数
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
// 设置与 key 绑定的特有数据块地址，成功返回 0， 失败返回一个正数
int pthread_setspecific(pthread_key_t key, const void *value);
// 返回与 key 绑定的数据块，没有绑定过返回 NULL
void *pthread_getspecific(pthread_key_t key);
```

一个通用的模板：

```c
#include <pthread.h>

static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t func_key;

static void destructor(void *buf) {
    free(buf);
}

static void createKey(void) {
    int s;
    s = pthread_key_create(&func_key, destructor);
}

void func(void) {
    char *buf;	// 线程特有数据
    pthread_once(&once, createKey);
    buf = pthread_getspecific(func_key);
    if(buf == NULL) {
        buf = malloc(BUF_SIZE);
        pthread_setspecific(func_key, buf);
    }
    // 使用线程特有数据
}
```

需要注意的是，key 的数量是有限制的。

#### 线程局部存储

与线程特有数据类似，线程局部存储提供了持久的没线程存储。但这个是非标准特性，在某些 Unix 发行版上可能不被支持。 只需要通过特殊的声明即可创建线程局部变量：

```c
static __thread type buf[MAX_ERROR_LEN];
```

带有 `__thread`说明符的变量，每个线程都拥有该变量的拷贝，此变量将一直存在，直至线程终止。

### 线程取消

线程取消是指，向线程发送一个请求，要求其立即退出。

```c
#include <pthread.h>
// 向线程发送取消请求，成功返回 0，失败返回一个正值
int pthread_cancel(pthread_t thread);
```

通过以下函数可以帮助线程对取消请求的响应过程加以控制：

```c
#include <pthread.h>

/*
@brief	将调用线程的取消性状态设置为参数 state 给定的值，并将原状态通过 oldstate 返回
    	state 可取
        - PTHREAD_CANCEL_DISABLE	线程不可取消，收到取消请求则挂起此请求，直到启用线程取消请求
        - PTHREAD_CANCEL_ENABLE		线程可以取消，默认值
*/
int pthread_setcancelstate(int state, int *oldstate);
/*
@brief	如果线程可以取消，以下函数规定了线程取消的方式
    	type 可取
        - PTHREAD_CANCEL_ASYNCHRONOUS	异步取消
        			        可能会在任何时间（也许是立即取消）取消线程
                                        此时清理函数仍然会执行，但函数当前状态是不确定的
                                        一般可异步取消的线程不能分配任何资源和互斥量或锁
        - PTHREAD_CANCEL_DEFERED        取消请求保持挂起，直到取消点，为默认类型
*/
int pthread_setcanceltype(int type, int *oldtype);
```

线程调用 fork 时，子进程会继承调用线程的取消性类型及状态。 SUSv3 规定必须是取消点的函数为： ![image.png](https://cdn.nlark.com/yuque/0/2023/png/29221640/1688384785232-d373c808-c768-4443-ba44-844e6d854ef9.png#averageHue=%23f3f3f3\&clientId=u6d672197-5e2a-4\&from=paste\&height=529\&id=u8887060b\&originHeight=925\&originWidth=1444\&originalType=binary\&ratio=1.75\&rotation=0\&showTitle=false\&size=291984\&status=done\&style=none\&taskId=ufe9194a4-6784-4056-91ba-4fc84760980\&title=\&width=825.1428571428571) 也可以通过以下函数自己设置取消点：

```c
#include <pthread.h>
// 在此处创建一个取消点
void pthread_testcancel();
```

每个线程都有一个清理函数栈，在收到取消请求时，线程会自动调用函数栈内的函数。

```c
#include <pthread.h>
// 向函数栈内添加函数
void pthread_cleanup_push(void (*routine)(void*), void *arg);
// 将函数栈顶部的函数弹出，execute 非 0 时还会执行这个弹出的函数
void pthread_cleanup_pop(int execute);
```

这两个函数在某些系统上可能用宏实现，所以这两个配对的操作需要在同一个语法块。例如，以下代码就不正确：

```c
pthread_cleanup_push(func, arg);
...
if(cond) {
    pthread_cleanup_pop(0);
}
```

### 一些细节

#### 信号、进程、线程

Unix 信号远早于 Pthreads，所以信号与线程模型之间存在一些冲突。在实际开发中，多线程模型应尽量避免使用信号。 信号与线程有以下注意点：

* 信号动作属于进程层面。进程的任一线程收到动作为 stop 或 terminate 的信号，则会终止该进程所有线程。
* 对信号的处置属于进程层面，进程中的所有线程共享对每个信号的处置设置。如果使用 `sigaction`为某信号设置了处理函数，当收到信号时，任何线程都会去调用该处理函数。
* 信号的发送也可以针对某个线程，下面几种信号是面向线程的：
  * 信号的产生源于线程上下文对特定硬件指令的执行（SIGBUS，SIGFPE，SIGKILL，SIGSEGV）
  * 线程对已断开的管道进行写操作时产生的 SIGPIPE
  * 由 `pthread_kill` 或 `pthread_sigqueue` 发出的信号
  * 其他机制产生的信号全是面向进程的。
* 多线程程序收到一个信号，只会选择一条线程来接收此信号。
* 信号掩码是针对线程的。
* 进程挂起信号，以及每条线程挂起的信号，内核分别维护记录。

#### 操作线程掩码

刚创建的新线程会从其创建者处继承信号掩码的一份拷贝。

```c
#include <signal.h>
// 与 sigprocmask 用法完全一致
// 成功返回 0，失败返回一个正数
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
```

#### 向线程发送信号

```c
#include <signal.h>
// 向线程发送 thread 发送 sig 信号
// 成功返回 0，失败返回一个正数
int pthread_kill(pthread_t thread, int sig);

#define _GNU_SOURCE
#include <signal.h>
// 将 pthread_kill 与 sigqueue 功能合并
// 成功返回 0，失败返回一个正数
int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);
```

#### 多线程中处理异步信号

没有任何 Pthreads API 为异步信号安全函数，所以不能在信号处理函数中加以调用。所以多线程程序中处理异步信号，通常不应该将信号处理函数作为接收信号到达的通知机制，一般推荐的方法如下：

* 所有线程都阻塞可能接受的所有异步信号，一般在创建线程之前由主线程阻塞，新线程会拥有一份拷贝。
* 再创建一个专用线程，调用 `sigwaitXXX`函数接收信号。

当收到信号时，专有线程可以安全地修改共享变量（结合互斥量），并可以调用并非异步信号安全的函数，也可以就条件变量发出信号。 如有多个线程在等待同一信号，那么信号到达时只有一个线程会实际接收到，并且无法确定收到信号的会是哪一条线程。

#### 线程与进程控制

这里主要描述线程执行 `exec`，`fork`，`exit`后的一些注意点：

* 线程与 `exec`：任一线程调用 `exec`系列函数后，调用程序将被完全替换。除了调用 `exec`的线程外，其他线程立即消失，且不会为线程特有函数执行解构，与清理函数。进程所有的互斥量和条件变量均会消失。调用 `exec`后，调用线程的线程 ID 是不确定的。
* 线程与 `fork`：多线程程序调用 `fork` 时仅会将发起调用的线程复制到子进程中，其他线程均在子进程中消失，也不会执行相关的解构函数，会导致一些问题：进程的互斥量等也会复制到子进程中，会导致十分棘手的问题产生。函数 `pthread_atfork`可以注册函数，在调用 `fork` 的之前执行注册函数。
* 线程与 `exit`：任何线程调用了 `exit`，所有线程均消失，不会执行特有数据的解构。

#### 线程实现模型

* 多对一（M：1）实现（用户级线程）：线程创建、调度、同步等均在用户空间的线程库处理，内核并不知道多线程的存在。优势在于速度十分快，因为不需要切换到内核状态，不需要内核支持，移植比较容易。也存在一些缺陷：一个线程阻塞，则所有线程都被阻塞。
* 一对一（1：1）实现（内核级线程）：此实现中，每一个线程映射一个单独的 KSE，内核对每个线程做调度处理。1：1 实现消除了 M：1 实现的各种弊端，尽管速度没有用户级线程快，但综合上还是优于用户及线程。NPTL 采用的为 1：1 模型。
* 多对多（M：N）实现（两级模型）：旨在结合前两个模型的优点，但其设计过于复杂。

对于 Pthreads API，Linux 有过两种实现：

* LinuxThreads：最初的实现，已经过时
* NPTL(Native POSIX Threads Library)：始于 Linux 2.6 版本，现在所有线程库开发均基于此，之前介绍的也为 NPTL 多线程实现。
