第31章 线程安全和线程存储
线程安全:再论可重入性
导致线程不安全的原因是:使用了所有线程之间共享的全局或静态变量
实现线程的方式:其一是将函数与互斥量关联使用、其二是将共享变量与互斥量关联起来
相比较于整个函数使用互斥量,使用临界区实现线程安全虽有改善,还是有些低效,而可重入函数则无需使用互斥量即可实现线程安全,其要诀在于避免对全局或静态变量的使用,需要返回给调用者的信息,都存储于调用者分配的缓冲区内,不过并非所有函数都可以实现为可重入,原因如下:
- 有些函数必须访问全局数据结构,如malloc函数库
- 一些函数的接口在线程发明之前本身就被定为为不可重入,如asctime
对于后缀是_r的可重入函数,要求由调用者分配缓冲区
一次性初始化
多线程程序有时不管创建多少个线程,有些初始化只做一次,pthread_once可以被调用多次,而init只会被调用一次,会被哪个线程调用,由内核调度决定
#include <pthread.h>
int pthread_once(pthread_once_t *once_control, void (*init)(void));
// 返回值:若成功,返回0,若出错,返回负值
线程特有数据
使用线程特有的数据技术,可以无需修改函数接口而实现已有函数的线程安全,比较于可重入函数,采用线程特有数据的函数效率要低,但是却省去了修改程序的麻烦;管理线程特定数据可以提高线程间的数据独立性
接口
int pthread_key_create(pthread_key_t *keyp, void (* _Nullable)(void *));
int pthread_key_delete(pthread_key_t keyp);
// 两个函数返回值:若成功,返回0,若出错,返回错误编号
// keyp可以被进程中所有线程使用,但是每个线程把这个键与不同的线程特定数据地址进行关联
// pthread_key_create可以为该键关联一个可选的析构函数,当线程调用pthread_exit或执行返回正常退出时,析构函数就会调用,线程取消时,只有在最后的清理处理程序返回之后,析构函数才会被调用,如果线程调用了exit、_exit、_Exit或abort,或其他非正常退出时,不会调用析构函数
// pthread_key_delete用来取消键与线程特定数据之间的关联,调用它并不会激活该键关联的析构函数
需要确保分配的键不会由于初始化阶段的竞争而发生变动,这样会导致有些线程看到的是一个键值,其他线程看到的是不同的键值,解决竞争的办法是使用pthread_once;键一旦创建,就可以通过pthread_setspecific把键和线程特定数据关联起来
void* _Nullable pthread_getspecific(pthread_key_t key);
// 返回线程特定数据,若没有值与该键关联,返回NULL
int pthread_setspecific(pthread_key_t key, const void * _Nullable value);
// 若成功,返回0,若出错,返回错误编号
线程局部存储
主要优点:比线程特有数据使用简单,要创建线程局部变量,只要在全局或静态变量的声明中包含__thread说明符即可,但凡带有这个标志,每个线程都拥有一份对该变量的拷贝,直到线程线程终止才会释放,关于其声明和使用的规则:
- 如果变量声明使用了static或extern,那么关键字__thread必须紧随其后
- 线程局部变量在声明时可以设置初始值
- 可以使用&来获取线程局部变量的地址