线程特有数据
- 实现函数线程安全最有效的方法是使其可冲入,应以这种方式来实现所有新的函数库。不过,对于线程引入之前的那些不可重入的库函数来说,采用这种方法通常需要修改函数接口,这也意味着,需要修改所有使用此类的函应用
- 为此,引入了线程特有数据技术,可以无需修改函数接口而实现已有函数的线程安全。
如果需要在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量,这就需要新的机制来实现,我们称之为Static memory local to a thread (线程局部静态变量),同时也可称之为线程特有数据(TSD: Thread-Specific Data)或者线程局部存储(TLS: Thread-Local Storage)。
线程特有数据技术使得函数得以为每个调用线程分别维护一份变量的副本(copy)。线程特有数据是长期存在的。在同一线程对相同函数的历次调用期间,每个线程的变量都会持续存在,函数可以向每个调用线程返回各自的结构缓冲区。每个线程可以修改自己而不影响其他线程。如下图:
库函数视角下的线程特有数据
要了解线程特有数据相关API的使用,需要从使用这一技术的库函数角度来考虑如下问题。
- 该函数必须为每个调用者线程分配单独的存储,而且只需在线程初次调用此函数时分配一次即可
- 在同一线程对此函数的后继所有调用中,该函数都需要获取初次调用时线程分配的存储块地址,由于函数调用结束时会释放自动变量,故而函数不应利用自动变量存放存储块指针,也不能将指针存放在静态变量中,因为静态变量在进程中只有一个实例。Pthreads API 提供了函数来处理这一情况。
- 不同(无相互依赖关系)函数各自可能都需要使用线程特有数据。每个函数都需要方法来标识其自身的线程特有数据(键),以便与其他函数所用的线程特有数据有所区分
- 当线程退出时,函数无法控制将要发生的情况。这时,线程可能会指向该函数之外的代码。不过,一定存在某些机制(结构器),在线程退出时自动释放为该线程所分配的内存。若非如此,随着持续不断地创建线程,调用函数和终止线程,将会引发内存
泄露
线程特有数据 API 概述
要使用线程特有数据,库函数执行的一般动作如下:
- 函数使用pthread_key_create()创建一个key,用以将不同函数使用的线程特有项区分开来。这个key只在首次调用该函数的线程中创建一次,key创建时并未分配任何线程特有数据块
- 调用pthread_key_create()还有另一个目的,即允许调用者指定一个自定义解构函数,用于释放为该key分配的各个存储块。当使用线程特有数据的线程终止时,pthread API将会自动调用此函数,同时将该线程的数据块指针作为参数传入
- 函数会为每个调用者线程创建线程特有数据块。这一分配通过调用malloc()或者类似函数完成,每个线程只分配一次,而且只会在线程初次调用此函数时分配
- 为了保存上一步所分配存储块的地址,函数只会使用两个pthreads函数:pthread_setspecific()和pthread_getspecific()。调用函数pthread_setspecific()实际上是对Pthreads实现发起这样的请求:保存该指针,并记录其与特定key(该函数的键)以及特定线程程(调用者线程)的关联性。调用 pthread_getspecific()所执行的是互补操作:返回之前所保存的、与给定键以及调用线程相关联的指针。如果还没有指针与特定的键及线程相关联,那么 pthread_getspecific()返回 NULL。函数可以利用这一点来判断自身是否是初次为某个线程所调用,如果是,则必须为该线程分配空间
线程特有数据 API 详解
在Linux中提供了如下函数和数据结构来对线程局部数据进行操作
相关数据结构
- pthread_key_t
/* Keys for thread-specific data */
typedef unsigned int pthread_key_t;
pthread_key_t
无论是哪一个线程创建,其他所有的线程都是可见的,即一个进程中只需phread_key_create()
一次- 看似是全局变量,然而全局的只是key值,对于不同的线程对应的value值是不同的(通过
pthread_setspcific
和pthread_getspecific
设置)
相关API
调用pthread_key_create()函数为线程特有数据创建一个新key,并通过key所指向的缓冲区返回给调用者。
#include <pthread.h>
/*
* 功能: 分配用于标识进程中线程特定数据的键。
* 参数: key 在分配( malloc )线程私有数据之前,需要创建和线程私有数据相关联的键( key ),这个键的功能是获得对线程私有数据的访问权
* destructor 清理函数名字( 如:fun )。当线程退出时,如果线程私有数据地址不是非 NULL,此函数会自动被调用。该函数指针可以设成 NULL ,这样系统将调用默认的清理函数。
* 返回值: 成功0,失败非0
*/
int pthread_key_create (pthread_key_t *key,
void (*destructor) (void *))
- 不论哪个线程调用 pthread_key_create(),所创建的 key 都是所有线程可访问,但各个线程可根据自己的需要往 key 中填入不同的值,相当于提供了一个同名不同值的变量。
- 因为进程中的所有线程都可以使用返回的键,所以key应该指向一个全局变量
- destructor指向一个自定义函数。只要线程终止时与key的关联值不为NULL,Pthreads API会自动执行解构函数,并将与key的关联值作为参数传入解构函数。如果无需解构,可以将destructor设置为NULL
- 如果一个线程有多个线程特有数据块,那么对各个解构函数的调用顺序是不固定的,每个解构函数的设计应该相互独立
函数 pthread_setspecific()要求 Pthreads API 将 value 的副本存储于一数据结构中,并将 value 与调用线程以及 key 相关联(key 由之前对pthread_key_create()的调用返回)。
pthread_getspecific()函数执行的操作与之相反,返回之前与本线程及给定 key 相关的值(value)
/*
功能: 设置线程私有数据( key ) 和 value 关联,注意,是 value 的值(不是所指的内容)和 key 相关联。
参数: key: 线程私有数据。
value: 指向由调用者分配的一块内存。 当线程终止时,会将该指针作为参数传递给与key对应的解构函数
参数 value也可以不是一个指向内存区域的指针,而是任何可以赋值(通过强制转换)
给 void*的标量值。在这种情况下,先前对 pthread_key_create()函数的调用应将 destructor
指定为 NULL。
返回值: 成功0,失败非0
*/
int pthread_setspecific(pthread_key_t key, const void *value);
/*
* 功能: 注销线程私有数据。这个函数并不会检查当前是否有线程正使用线程私有数据( key ),也不会调用清理函数 destructor() ,而只是将线程私有数据( key )释放以供下一次调用 pthread_key_create() 使用。
* 参数: 待注销的私有数据
* 返回值: 成功0,失败非0
*/
int pthread_key_delete (pthread_key_t __key) __THROW
- 由于系统对每个进程中pthread_key_t类型的个数是有限制的,所以进程中并不能创建无限个的pthread_key_t变量。
- Linux中可以通过PTHREAD_KEY_MAX(定义于limits.h文件中)或者系统调用sysconf(_SC_THREAD_KEYS_MAX)来确定当前系统最多支持多少个键。Linux中默认是1024个键,这对于大多数程序来说已经足够了。
- 如果一个线程中有多个线程局部存储变量,通常可以将这些变量封装到一个数据结构中,然后使封装后的数据结构与一个线程局部变量相关联,这样就能减少对键值的使用。
那该怎么用呢?
- 创建一个连续为
pthread_ket_t
类型的变量 - 使用
pthread_key_create()
来创建该变量。 - 当线程中需要存储特殊值的时候,可以调用
pthread_setspecific
- 如果需要取出所存储的值,调用
pthread_getspecific
深入理解:在典型的实现中包含以下数组:
-
一个全局(进程级别)的数组,用于存放线程局部存储的键值信息
pthread_key_create
返回的pthread_key_t
类型值只是对全局数组的索引,该全局数组标记为pthread_keys
,其格式大概如下
- 数组的每个元素都是一个包含两个字段的结构,
- 第一个字段标记该数组元素是否在用,(即已由之前对 pthread_key_
create()的调用分配) - 第二个字段用于存放针对此key、线程局部存储的解构函数的一个副本,即
destructor
函数的拷贝
- 第一个字段标记该数组元素是否在用,(即已由之前对 pthread_key_
-
每个线程还包含一个数组,存有为每个线程分配的线程特有数据块的指针(通过调用pthread_setspecific()函数来存储的指针,即参数中的value)
-
在常见的存储pthread_setspecific()函数参数value的实现中,大多数都类似于下图的实现。图中假设pthread_keys[1]分配给func1()函数,pthread API为每个函数维护指向线程局部存储数据块的一个指针数组,其中每个数组元素都与图线程局部数据键的实现(上图)中的全局pthread_keys中元素一一对应
static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t strerrorKey;
#define MAX_ERROR_LEN 256 /* Maximum length of string in per-thread
buffer returned by strerror() */
static void destructor(void *buf) /* Free thread-specific data buffer */
{
free(buf);
}
static void createKey(void) /* One-time key creation function */
{
int s;
/* Allocate a unique thread-specific data key and save the address
of the destructor for thread-specific data buffers */
s = pthread_key_create(&strerrorKey, destructor);
if (s != 0){
printf( "%d pthread_key_create", s);
exit(1);
}
}
char * strerror(int err)
{
int s;
char *buf;
/* Make first caller allocate key for thread-specific data */
s = pthread_once(&once, createKey);
if (s != 0){
printf( "%d pthread_once", s);
exit(1);
}
buf = pthread_getspecific(strerrorKey);
if (buf == NULL) { /* If first call from this thread, allocate
buffer for thread, and save its location */
buf = malloc(MAX_ERROR_LEN);
if (buf == NULL){
perror("malloc");
exit(1);
}
s = pthread_setspecific(strerrorKey, buf);
if (s != 0){
printf( "%d pthread_setspecific", s);
exit(1);
}
}
if (err < 0 || err >= _sys_nerr || _sys_errlist[err] == NULL) {
snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", err);
} else {
strncpy(buf, _sys_errlist[err], MAX_ERROR_LEN - 1);
buf[MAX_ERROR_LEN - 1] = '\0'; /* Ensure null termination */
}
return buf;
}
#include<fstream>
#include<iomanip>
#include <zconf.h>
pthread_key_t key;
void echomsg(int t) {
printf("destructor excuted in thread %lu,param=%d\n", pthread_self(), t);
}
void * child1(void * arg){
pthread_t tid =pthread_self();
// printf("pid - %d start\n", (unsigned int )tid);
pthread_setspecific(key,(void *)tid);
sleep(2);
printf("thread %u returns %d and key %d\n",(unsigned int )tid,pthread_getspecific(key),key);
// printf("pid - %d end\n", (unsigned int )tid);
}
void * child2(void * arg)
{
pthread_t tid =pthread_self();
// printf("pid - %d start\n", (unsigned int )tid);
pthread_setspecific(key,(void *)tid);
sleep(1);
printf("thread %u returns %d and key %d\n",(unsigned int )tid,pthread_getspecific(key),key);
printf("pid - %d end\n", (unsigned int )tid);
//getchar();
}
int main(int arcg, char** argv)
{
pthread_t tid1,tid2;
printf("main start\n");
pthread_key_create(&key,(void (*)(void *))echomsg);
pthread_create(&tid1,NULL,child1,NULL);
pthread_create(&tid2,NULL,child2,NULL);
sleep(10);
pthread_key_delete(key);
printf("end start\n");
return 0;
}
#include <malloc.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
pthread_key_t thread_log_key;
/*通用函数里可以利用 pthread_getspecific() 处理线程各自的私有数据*/
void write_to_thread_log ( const char * message)
{
FILE * thread_log = ( FILE *) pthread_getspecific ( thread_log_key);
fprintf ( thread_log , "%s \n " , message);
}
void close_thread_log ( void * thread_log)
{
fclose (( FILE *) thread_log);
}
void * thread_function ( void * args)
{
char thread_log_filename [ 128 ];
char thread_start_message [ 128 ];
FILE * thread_log;
sprintf ( thread_log_filename , "thread%u.log" , pthread_self());
thread_log = fopen ( thread_log_filename , "w");
pthread_setspecific ( thread_log_key , thread_log); //每个线程都设置自己的私有数据
sprintf ( thread_start_message , "thread %u starting" , pthread_self());
write_to_thread_log ( thread_start_message);
pthread_exit( NULL);
}
int main()
{
int i;
pthread_t threads [ 5 ];
/*创建私有数据键,close_thread_log 在线程退出时对 key 关联数据进行清理*/
pthread_key_create ( & thread_log_key , close_thread_log);
for ( i = 0; i < 5; i ++)
pthread_create ( & threads [ i ], NULL , thread_function , NULL); //创建多线程
for ( i = 0; i < 5; i ++)
pthread_join ( threads [ i ], NULL); //等待各个线程结束
return ( 0);
}
线程局部存储
类似线程特有数据,线程局部存储提供了持久的每线程存储。。作为非标准特性,诸多其他的 UNIX 实现(例如 Solaris 和 FreeBSD)为其提供了相同,或类似的接口形式
线程局部存储的主要优点在于,比线程特有数据的使用要简单。要创建线程局部变量,只需要简单的在全局或者静态变量的声明中包含__thread
说明符即可:
static __thread buf[MAX_ERROR_LEN];
但凡带有这种说明符的变量,每个变量都拥有一份对变量的拷贝。线程局部存储中的变量将一直存在,直到线程终止,届时会自动释放这一存储。
关于线程局部变量的声明和使用,需要注意如下几点。
- 如果变量声明中使用了关键字 static 或 extern,那么关键字__thread 必须紧随其后
- 与一般的全局或静态变量声明一样,线程局部变量在声明时可设置一个初始值
- 可以使用 C 语言取址操作符(&)来获取线程局部变量的地址。
线程局部存储需要内核(由 Linux 2.6 提供)、Pthreads 实现(由 NPTL 提供)以及 C 编译器(在 x86-32 平台上由 gcc 3.3 或后续版本提供)的支持
使用示例:
#define _GNU_SOURCE /* Get '_sys_nerr' and '_sys_errlist'
declarations from <stdio.h> */
#include <stdio.h>
#include <string.h> /* Get declaration of strerror() */
#include <pthread.h>
#define MAX_ERROR_LEN 256 /* Maximum length of string in per-thread
buffer returned by strerror() */
static __thread char buf[MAX_ERROR_LEN];
/* Thread-local return buffer */
char *
strerror(int err)
{
if (err < 0 || err >= _sys_nerr || _sys_errlist[err] == NULL) {
snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", err);
} else {
strncpy(buf, _sys_errlist[err], MAX_ERROR_LEN - 1);
buf[MAX_ERROR_LEN - 1] = '\0'; /* Ensure null termination */
}
return buf;
}
总结
- 如果一个函数可以由多个线程同时安全调用,则称之为线程安全的函数。使用全局或者静态变量是导致函数非线程安全的通常原因。在多线程应用中,保障非线程安全函数安全的方法之一是运用互斥锁来防护该该函数的所有调用。这种方法带来了并发性能的下降,因为同一时点只能有一个线程运行该函数。提升并发性能的另一方法是:仅在函数中操作共享变量量(临界区)的代码前后加入互斥锁
- 使用互斥量可以实现大部分函数的线程安全,不过由于互斥量的加、解锁开销,故而也
带来了性能的下降。如能避免使用全局或静态变量,可重入函数则无需使用互斥量即可实现线程安全 - SUSv3 所规范的大部分函数都需实现线程安全。SUSv3 同时也列出了小部分无需实现线
程安全的函数。一般情况下,这些函数将静态存储返回给调用者,或者在对函数的连续调用间进行信息维护。根据定义,这些函数是不可重入的,也不能使用互斥量来确保其线程安全。线程特有数据和线程局部存储——可在无需改变函数接口定义的情况下保障不安全函数的线程安全。这两种技术均允许函数分配持久的、基于线程的存储