4.2 线程安全、线程取消

线程安全、线程取消以及一些额外细节

线程安全

线程安全函数

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

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

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

一次性初始化

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

#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_setspecificpthread_getspecific设置和获取。一般先获取数据地址,若为 NULL 说明还没有分配,然后使用 pthread_setspecific设置数据地址。

一个通用的模板:

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

线程局部存储

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

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

线程取消

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

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

线程调用 fork 时,子进程会继承调用线程的取消性类型及状态。 SUSv3 规定必须是取消点的函数为: image.png 也可以通过以下函数自己设置取消点:

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

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

一些细节

信号、进程、线程

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

  • 信号动作属于进程层面。进程的任一线程收到动作为 stop 或 terminate 的信号,则会终止该进程所有线程。

  • 对信号的处置属于进程层面,进程中的所有线程共享对每个信号的处置设置。如果使用 sigaction为某信号设置了处理函数,当收到信号时,任何线程都会去调用该处理函数。

  • 信号的发送也可以针对某个线程,下面几种信号是面向线程的:

    • 信号的产生源于线程上下文对特定硬件指令的执行(SIGBUS,SIGFPE,SIGKILL,SIGSEGV)

    • 线程对已断开的管道进行写操作时产生的 SIGPIPE

    • pthread_killpthread_sigqueue 发出的信号

    • 其他机制产生的信号全是面向进程的。

  • 多线程程序收到一个信号,只会选择一条线程来接收此信号。

  • 信号掩码是针对线程的。

  • 进程挂起信号,以及每条线程挂起的信号,内核分别维护记录。

操作线程掩码

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

向线程发送信号

多线程中处理异步信号

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

  • 所有线程都阻塞可能接受的所有异步信号,一般在创建线程之前由主线程阻塞,新线程会拥有一份拷贝。

  • 再创建一个专用线程,调用 sigwaitXXX函数接收信号。

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

线程与进程控制

这里主要描述线程执行 execforkexit后的一些注意点:

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

最后更新于

这有帮助吗?