#include <pthread.h>
// Returns 0 on success, or a positive error number on error
int pthread_once (pthread_once_t *once_control, void (*init)
(void));
利用参数once_control的状态,函数pthread_once()可以确保无论有多少个线程调用多少次该函数,也只会执行一次由init所指向的由调用者定义的函数。init所指向的函数没有任何参数,形式如下:
void init (void)
{
// some variables initializtion in here
}
三、线程局部数据API
在Linux中提供了如下函数来对线程局部数据进行操作
#include <pthread.h>
// Returns 0 on success, or a positive error number on error
int pthread_key_create (pthread_key_t *key, void (*destructor)(void *));
// Returns 0 on success, or a positive error number on error
int pthread_key_delete (pthread_key_t key);
// Returns 0 on success, or a positive error number on error
int pthread_setspecific (pthread_key_t key, const
void *value);
// Returns pointer, or NULL if no thread-specific data is associated with key
void *pthread_getspecific (pthread_key_t key);
函数pthread_key_create()为线程局部数据创建一个新键,并通过key指向新创建的键缓冲区。因为所有线程都可以使用返回的新键,所以参数key可以是一个全局变量(在C++多线程编程中一般不使用全局变量,而是使用单独的类对线程局部数据进行封装,每个变量使用一个独立的pthread_key_t)。destructor所指向的是一个自定义的函数,其格式如下:
void Dest (void *value)
{
// Release storage pointed to by ‘value‘
}
只要线程终止时与key关联的值不为NULL,则destructor所指的函数将会自动被调用。如果一个线程中有多个线程局部存储变量,那么对各个变量所对应的destructor函数的调用顺序是不确定的,因此,每个变量的destructor函数的设计应该相互独立。
函数pthread_key_delete()并不检查当前是否有线程正在使用该线程局部数据变量,也不会调用清理函数destructor,而只是将其释放以供下一次调用pthread_key_create()使用。在Linux线程中,它还会将与之相关的线程数据项设置为NULL。
由于系统对每个进程中pthread_key_t类型的个数是有限制的,所以进程中并不能创建无限个的pthread_key_t变量。Linux中可以通过PTHREAD_KEY_MAX(定义于limits.h文件中)或者系统调用sysconf(_SC_THREAD_KEYS_MAX)来确定当前系统最多支持多少个键。Linux中默认是1024个键,这对于大多数程序来说已经足够了。如果一个线程中有多个线程局部存储变量,通常可以将这些变量封装到一个数据结构中,然后使封装后的数据结构与一个线程局部变量相关联,这样就能减少对键值的使用。
函数pthread_setspecific()用于将value的副本存储于一数据结构中,并将其与调用线程以及key相关联。参数value通常指向由调用者分配的一块内存,当线程终止时,会将该指针作为参数传递给与key相关联的destructor函数。当线程被创建时,会将所有的线程局部存储变量初始化为NULL,因此第一次使用此类变量前必须先调用pthread_getspecific()函数来确认是否已经于对应的key相关联,如果没有,那么pthread_getspecific()会分配一块内存并通过pthread_setspecific()函数保存指向该内存块的指针。
参数value的值也可以不是一个指向调用者分配的内存区域,而是任何可以强制转换为void*的变量值,在这种情况下,先前的pthread_key_create()函数应将参数
destructor设置为NULL
函数pthread_getspecific()正好与pthread_setspecific()相反,其是将pthread_setspecific()设置的value取出。在使用取出的值前最好是将void*转换成原始数据类型的指针。
四、深入理解线程局部存储机制
1. 深入理解线程局部存储的实现有助于对其API的使用。在典型的实现中包含以下数组:
- 一个全局(进程级别)的数组,用于存放线程局部存储的键值信息
pthread_key_create()返回的pthread_key_t类型值只是对全局数组的索引,该全局数组标记为pthread_keys,其格式大概如下:
数组的每个元素都是一个包含两个字段的结构,第一个字段标记该数组元素是否在用,第二个字段用于存放针对此键、线程局部存储变的解构函数的一个副本,即destructor函数。
- 每个线程还包含一个数组,存有为每个线程分配的线程特有数据块的指针(通过调用pthread_setspecific()函数来存储的指针,即参数中的value)
2. 在常见的存储pthread_setspecific()函数参数value的实现中,大多数都类似于下图的实现。图中假设pthread_keys[1]分配给func1()函数,pthread API为每个函数维护指向线程局部存储数据块的一个指针数组,其中每个数组元素都与图线程局部数据键的实现(上图)中的全局pthread_keys中元素一一对应。

五、总结
使用全局变量或者静态变量是导致多线程编程中非线程安全的常见原因。在多线程程序中,保障非线程安全的常用手段之一是使用互斥锁来做保护,这种方法带来了并发性能下降,同时也只能有一个线程对数据进行读写。如果程序中能避免使用全局变量或静态变量,那么这些程序就是线程安全的,性能也可以得到很大的提升。如果有些数据只能有一个线程可以访问,那么这一类数据就可以使用线程局部存储机制来处理,虽然使用这种机制会给程序执行效率上带来一定的影响,但对于使用锁机制来说,这些性能影响将可以忽略。Linux
C++的线程局部存储简单实现可参考https://github.com/ApusApp/Swift/blob/master/swift/base/threadlocal.h,更详细且高效的实现可参考Facebook的folly库中的ThreadLocal实现。更高性能的线程局部存储机制就是使用__thread,这将在下一节中讨论。
参考:
[1] Linux/UNIX系统编程手册(上)
[2] http://www.groad.net/bbs/thread-2182-1-1.html
[3] http://baike.baidu.com/view/598128.htm