线程特定数据,也被称为线程私有数据,是一种存储和查找一个特定线程相关数据的机制。我们称这个数据为线程特定或线程私有的原因,是因为每个线程访问它自己独立的数据拷贝,而不用担心和其它线程的访问的同步。线程特定数据看似很复杂,其实我们可以把它理解为就是一个索引和指针。key结构中存储的是索引,指针value则指向线程中的私有数据,通常是malloc函数返回的指针。
假设有以下函数:
#define _GUN_SOURCE #include<stdio.h> #include<string.h> #include<errno.h> #define MAX_ERROR_LEN 256 static char buf[MAX_ERROR_LEN]; //mystrerror返回错误码errno表示的字符串 char *mystrerror(int err) { if (err < 0 || err >= _sys_nerr || _sys_errlist[err] == NULL) { snprintf(buf, MAX_ERROR_LEN, "Unknow error %d", err); } else { strncpy(buf, _sys__errlist[err], MAX_ERROR_LEN - 1); buf[MAX_ERROR_LEN - 1] = '\0'; } return buf; }
可以看到mystrerror函数返回由错误码errno表示的字符串,而该字符串的内存来源于一块全局的静态内存块。我们知道全局静态内存块是进程内所有线程可以共同修改即访问的,由于线程的并发性,可能会导致某个线程正在用着这块内存的时候,被另一个线程给修改了,从而导致数据产生混乱,我们通过实例来感受一下,假设有两个线程使用mystrerror函数:
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<pthread.h> #include<errno.h> #define handle_error_en(en, msg)\ do {errno = en; perror(msg); exit(EXIT_FAILURE);} while(0); extern char *mystrerror(int err); static void *threadFunc(void *arg) { char *str; printf("Other thread about call mystrerror()\n"); str = mystrerror(EPERM); printf("Other thread: str (%p) = %s\n", str, str); return NULL; } int main() { pthread_t t; int s; char *str; str = mystrerror(EINVAL); printf("Main thread has called mystrerror()\n"); s = pthread_create(&t, NULL, threadFunc, NULL); if (0 != s) { handle_error_en(s, "pthread_create"); } s = pthread_join(t, NULL); if (0 != s) { handle_error_en(s, "pthread_join"); } printf("Main thread: str (%p) = %s\n", str, str); exit(EXIT_SUCCESS); }
运行结果:
从上面的演示我们看到,主线程和子线程获取到的都是错误码EPERM的字符串信息。这种结果明显不是我们想要的,我们希望主线程获取的是EINVAL对应的字符串信息,子线程获取到的是EPERM对应的字符串信息。
解决以上问题的其中一种方法,就是使用线程特有数据;所谓的线程特有数据,说白了,就是一块看起来是全局的数据概念,但是其实每个线程都有其特有的数据内容,因此每个线程都需要各自不受干扰的内存用来存放数据。线程特有数据的接口如下:
#include <pthread.h> int pthread_key_create(pthread_key_t *key, void (*destructor)(void *)); int pthread_setspecific(pthread_key_t key, const void *value); void *pthread_getspecific(pthread_key_t key);
int pthread_key_delete (pthread_key_t key);
函数说明:
pthread_key_create:创建一个全局唯一key,用来表示一个数据概念。
pthread_setspecific, 用于线程给某个数据概念分配内存。
pthread_getspecific, 用于线程针对某个数据概念获取其对应的内存(每个线程获取的内存是不一样的),如果函数返回NULL值说明线程还未对该数据概念分配内存
接口使用思路如下:
- 先用pthread_key_create创建一个全局的key,用于表示一块全局的数据概念。
- 每个线程在使用该数据概念时,先通过pthread_getspecific查询该线程是否为该数据概念分配了内存
- 如果线程未对该数据概念分配内存,使用pthread_setspecific为该数据概念分配特有内存
- 如果线程已对该数据概念分配内存,直接操作该内存。
由于一个数据概念对应一个key,即对于一个数据概念而言不管有多少个线程pthread_key_create仅需要被调用一次,因此pthread_key_create经常在pthread_once函数里被调用。
pthread_key_create函数中有一个参数destructor,提供了一种释放线程特有数据内存的机制,当某个线程针终止时,如果该线程针对该key分配了内存,那么destructor函数就会被调用,传递给destructor函数的参数就是该线程针对该key分配的内存指针。
修改程序
复制代码
#define _GNU_SOURCE
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<errno.h>
#include<pthread.h>
static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t strerrorKey;
#define handle_error_en(en, msg) \
do { errno = en; perror(msg); exit(EXIT_FAILURE);} while(0)
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); }while(0)
#define MAX_ERROR_LEN 256
//线程特有数据的析构函数
static void destructor(void *buf)
{
free(buf);
}
static void createKey(void)
{
int s;
/*在pthread_once函数里创建特有数据的key
*哪个线程先调用就哪个线程创建key
*/
s = pthread_key_create(&strerrorKey, destructor);
if (0 != s)
{
handle_error_en(s, "pthread_key_create");
}
}
char *mystrerror(int err)
{
int s;
char *buf;
//一次性初始化函数
s = pthread_once(&once, createKey);
if (0 != s)
{
handle_error_en(s, "pthread_once");
}
//获取线程特有数据
buf = pthread_getspecific(strerrorKey);
//第一次获取为NULL, 线程需要分配内存
if (buf == NULL)
{
buf = malloc(MAX_ERROR_LEN);
if (buf == NULL)
{
handle_error("malloc");
}
//设置内存特有数据内存
s = pthread_setspecific(strerrorKey, buf);
if (0 != s)
{
handle_error_en(s, "pthread_setspecific");
}
}
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';
}
return buf;
}
运行结果:
另外,可以使用线程局部储存比这简单得多,只需在全局或静态变量加修饰符__thread
例如:
static __thread int a=10; // (注意__thread的位置) extern __thread int b; // (注意__thread的位置)
带有__thread修饰符的变量,每个线程都拥有一份拷贝,且一直存在到线程终止,线程终止会自动释放这一变量。