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));
此函数经常与线程特有数据结合使用。
线程特有数据
线程特有数据为每个线程提供一份独有的数据副本。一般步骤如下:
函数创建一个键(key),用以区分不同函数使用的线程特有函数,可通过
pthread_key_create
创建,且只需要创建一次,所以需要配合pthread_once
使用。pthread_key_create
还允许调用者指定一个自定义解构函数,在线程终止时释放该线程的特有数据块使用 malloc 创建特有数据
上一步分配的数据地址可通过
pthread_setspecific
和pthread_getspecific
设置和获取。一般先获取数据地址,若为 NULL 说明还没有分配,然后使用pthread_setspecific
设置数据地址。
#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);
一个通用的模板:
#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 发行版上可能不被支持。 只需要通过特殊的声明即可创建线程局部变量:
static __thread type buf[MAX_ERROR_LEN];
带有 __thread
说明符的变量,每个线程都拥有该变量的拷贝,此变量将一直存在,直至线程终止。
线程取消
线程取消是指,向线程发送一个请求,要求其立即退出。
#include <pthread.h>
// 向线程发送取消请求,成功返回 0,失败返回一个正值
int pthread_cancel(pthread_t thread);
通过以下函数可以帮助线程对取消请求的响应过程加以控制:
#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 规定必须是取消点的函数为: 也可以通过以下函数自己设置取消点:
#include <pthread.h>
// 在此处创建一个取消点
void pthread_testcancel();
每个线程都有一个清理函数栈,在收到取消请求时,线程会自动调用函数栈内的函数。
#include <pthread.h>
// 向函数栈内添加函数
void pthread_cleanup_push(void (*routine)(void*), void *arg);
// 将函数栈顶部的函数弹出,execute 非 0 时还会执行这个弹出的函数
void pthread_cleanup_pop(int execute);
这两个函数在某些系统上可能用宏实现,所以这两个配对的操作需要在同一个语法块。例如,以下代码就不正确:
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
发出的信号其他机制产生的信号全是面向进程的。
多线程程序收到一个信号,只会选择一条线程来接收此信号。
信号掩码是针对线程的。
进程挂起信号,以及每条线程挂起的信号,内核分别维护记录。
操作线程掩码
刚创建的新线程会从其创建者处继承信号掩码的一份拷贝。
#include <signal.h>
// 与 sigprocmask 用法完全一致
// 成功返回 0,失败返回一个正数
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
向线程发送信号
#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 多线程实现。
最后更新于
这有帮助吗?