linux线程特有数据详解
线程特定数据,也被称为线程私有数据,是一种存储和查找一个特定线程相关数据的机制。我们称这个数据为线程特定或线程私有的原因,是因为每个线程访问它自己独立的数据拷贝,而不用担心和其它线程的访问的同步。线程特定数据看似很复杂,其实我们可以把它理解为就是一个索引和指针。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修饰符的变量,每个线程都拥有一份拷贝,且一直存在到线程终止,线程终止会自动释放这一变量。