# 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 多线程实现。


---

# 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.2-xian-cheng-an-quan-xian-cheng-qu-xiao.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.
