4.2 线程安全、线程取消
线程安全、线程取消以及一些额外细节
线程安全
线程安全函数
若函数可同时供多个线程安全调用,则称之为线程安全函数。如以下函数:
一般导致线程不安全的典型原因是:使用了所有线程之间共享的全局或静态变量。可通过互斥量与共享变量关联起来,已保证线程安全。
一次性初始化
一次性初始化是指,无论创建多少线程,初始化动作只发生一次:
此函数经常与线程特有数据结合使用。
线程特有数据
线程特有数据为每个线程提供一份独有的数据副本。一般步骤如下:
函数创建一个键(key),用以区分不同函数使用的线程特有函数,可通过
pthread_key_create
创建,且只需要创建一次,所以需要配合pthread_once
使用。pthread_key_create
还允许调用者指定一个自定义解构函数,在线程终止时释放该线程的特有数据块使用 malloc 创建特有数据
上一步分配的数据地址可通过
pthread_setspecific
和pthread_getspecific
设置和获取。一般先获取数据地址,若为 NULL 说明还没有分配,然后使用pthread_setspecific
设置数据地址。
一个通用的模板:
需要注意的是,key 的数量是有限制的。
线程局部存储
与线程特有数据类似,线程局部存储提供了持久的没线程存储。但这个是非标准特性,在某些 Unix 发行版上可能不被支持。 只需要通过特殊的声明即可创建线程局部变量:
带有 __thread
说明符的变量,每个线程都拥有该变量的拷贝,此变量将一直存在,直至线程终止。
线程取消
线程取消是指,向线程发送一个请求,要求其立即退出。
通过以下函数可以帮助线程对取消请求的响应过程加以控制:
每个线程都有一个清理函数栈,在收到取消请求时,线程会自动调用函数栈内的函数。
这两个函数在某些系统上可能用宏实现,所以这两个配对的操作需要在同一个语法块。例如,以下代码就不正确:
一些细节
信号、进程、线程
Unix 信号远早于 Pthreads,所以信号与线程模型之间存在一些冲突。在实际开发中,多线程模型应尽量避免使用信号。 信号与线程有以下注意点:
信号动作属于进程层面。进程的任一线程收到动作为 stop 或 terminate 的信号,则会终止该进程所有线程。
对信号的处置属于进程层面,进程中的所有线程共享对每个信号的处置设置。如果使用
sigaction
为某信号设置了处理函数,当收到信号时,任何线程都会去调用该处理函数。信号的发送也可以针对某个线程,下面几种信号是面向线程的:
信号的产生源于线程上下文对特定硬件指令的执行(SIGBUS,SIGFPE,SIGKILL,SIGSEGV)
线程对已断开的管道进行写操作时产生的 SIGPIPE
由
pthread_kill
或pthread_sigqueue
发出的信号其他机制产生的信号全是面向进程的。
多线程程序收到一个信号,只会选择一条线程来接收此信号。
信号掩码是针对线程的。
进程挂起信号,以及每条线程挂起的信号,内核分别维护记录。
操作线程掩码
刚创建的新线程会从其创建者处继承信号掩码的一份拷贝。
向线程发送信号
多线程中处理异步信号
没有任何 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 多线程实现。
最后更新于