线程安全存储以及pthread_getspecific/pthread_setspecific

最近阅读一份linux的线程代码时,看到了一套函数,pthread_getspecific/pthread_setspecific。光从名字上,完全无法理解出他们到底是干啥的,结合代码来看,也不是很清楚。于是就去百度。可是,百度来百度去,CSDN来CSDN去,反反复复找了不少资料,却始终没能完全理解透彻。感觉大家都是从同一份博客里抄来抄去的,甚至连其中的错误都照抄了,不但没能解答我的疑惑,反而还带来了其他的问题。

题外话一:这里实在想吐槽一下百度的学术资料搜索能力,跟人家google比,实在是差太多了,找半天都找不到什么有用的资料。广告搜索能力倒是挺强,各种推广广告一堆堆的跳出来,而且是在最显眼的地方。为什么不能把广告的搜索能力移植到学术资料的搜索上来呢?

题外话二:这里还想吐槽一下部分CSDN的网友,看到别人的博客,自己也不验证验证,消化消化,就直接复制黏贴过来,关键也不注明一下是不是转载的,更不用说贴上源地址了。我找资料时,一个关键词输进去,链接确实能跳出来不少,但是点进去发现TM全是同一篇文章,只是由不同的博主转来转去的,绝大部分人甚至连原博文的错误都一并转载了,实在是哭笑不得。

不管如何,自己的疑问还是要搞清楚的。于是继续查阅资料,并对这两个函数的用法进行总结和实践,总算是基本搞清了。下面对我自己的心得进行一下总结,希望能给其他小伙伴提供一下借鉴。

一、技术原理

首先要交代一下,pthread_getspecific/pthread_setspecific这两个函数,其实是属于线程数据存储机制【也叫线程私有数据,英文名Thread Specific Data】的一部分。因此,要先引入线程存储机制这个大话题,才能进行比较全面而清晰的了解。

大家都知道,在多线程程序中,一个模块里的全局变量,对于该模块的所有线程都是可见的,也就是说全局共享的,所有线程都可以使用它,可以随时改变它的值,这显然是不安全的,会带来一些风险。那么有没有什么办法能改善这个问题呢?在强大的linux系统下,一切皆有可能!这就到了线程存储机制隆重登场的时候了!应用了线程存储机制之后,这个变量表面上看起来还是一个全局变量,所有线程都可以使用它,而它的值在每一个线程中又是单独隔离的,彼此之间不会互相影响。A线程对该变量的值进行修改之后,相应的变化只会在A线程中体现,其他的线程中去查询该变量时,得到的依然是本线程中的值,跟A线程改动的值无关。

比如linux系统下常见的errno,它返回标准的错误码。errno肯定不是一个局部变量,因为几乎每个函数都可以访问他。但它又不能是一个简单的全局变量,否则在一个线程里输出的很可能是另一个线程的出错信息。因此,必须借助于创建线程的私有数据来解决。线程私有数据采用一键多值的技术,即一个键对应多个值。访问数据时都是通过键值来访问,表面上看上去好像是对同一个变量进行访问,但是实际上访问的是不同的数据空间。

线程存储机制的具体用法如下:

  1. 首先要定义一个类型为pthread_key_t类型的全局变量。
  2. 调用pthread_key_create()来创建该变量。该函数有两个参数,第一个参数就是上面声明的pthread_key_t变量,第二个参数是一个清理函数,用来在线程释放该线程存储的时候被调用。该函数指针可以设成NULL,这样系统将调用默认的清理函数。该函数调用成功时返回0,返回其他任何值都表示出现了错误。
  3. 当线程中需要存储特殊值的时候,可以调用pthread_setspcific()。该函数有两个参数,第一个为前面声明的pthread_key_t变量,第二个为void*变量,这样你可以存储任何类型的值。
  4. 如果需要取出所存储的值,调用pthread_getspecific()。该函数的参数为前面提到的pthread_key_t变量,该函数返回void *类型的值。
  5. 注销使用pthread_key_delete()函数,该函数并不检查当前是否有线程正在使用,也不会调用清理函数,而只是释放以供下一次调用pthread_key_create()使用。

下面是前面提到的函数原型:

#include <pthread.h>

int pthread_key_create(pthread_key_t *key, void (*destructor)(void *));
int pthread_key_delete(pthread_key_t *key);
int pthread_setspecific(pthread_key_t key, const void *value);
void *pthread_getspecific(pthread_key_t key);

二、实例解析

下面的代码是从网上摘录过来,内容很简单,就是用来验证这两个函数的使用。

新建一个模块命名为test.c,输入以下内容:

#include <stdio.h>
#include <pthread.h>
#include <string.h>

static pthread_key_t p_key;
 
void *thread_func(void *args)
{
    int *tmp1, *tmp2;
    
    /* 注意:这里初始化私有键值时使用的特征值,是该线程创建时传入的特征值a/b */
    pthread_setspecific(p_key, args);                                          /* 初始化本线程的私有键值 */
    printf("in thread %d. init specific_data to %d\n", *(int *)args, *(int *)args);
    
    tmp1 = (int *)pthread_getspecific(p_key);                                  /* 获取私有键值的内容 */
    printf("get specific_data %d\n", *tmp1);
 
    *tmp1 = (*tmp1) * 100;                                                     /* 修改私有键值的内容 */
    printf("change specific_data to %d\n", *tmp1);

    tmp2 = (int *)pthread_getspecific(p_key);                                  /* 重新获取本线程的私有键值 */
    printf("get specific_data %d\n", *tmp2);
 
    return (void *)0;
}

int main()
{
    int a = 1;
    int b = 2;
    pthread_t pa, pb;
    
    printf("at first: a = %d. b = %d\n", a, b);

    pthread_key_create(&p_key, NULL);                                          /* 首先创建私有数据键值 */
    
    pthread_create(&pa, NULL, thread_func, &a);                                /* 创建线程1 */
    pthread_create(&pb, NULL, thread_func, &b);                                /* 创建线程2 */

    pthread_join(pa, NULL);
    pthread_join(pb, NULL);
    
    /* 解释下pthread_join函数:它的目的是使一个线程等待另一个线程结束 */
    /* 代码中如果没有pthread_join主线程会很快结束从而使整个进程结束,创建的线程根本来不及执行 */
    /* 加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行 */
    
    printf("at last: a = %d. b = %d\n", a, b);

    return 0;
}

编译链接并运行,可以看到输出信息如下:

leon@Ubuntu:~/test$ gcc test.c -lpthread -o test
leon@Ubuntu:~/test$ ./test
at first: a = 1. b = 2
in thread 1. init specific_data to 1
get specific_data 1
change specific_data to 100
get specific_data 100
in thread 2. init specific_data to 2
get specific_data 2
change specific_data to 200
get specific_data 200
at last: a = 100. b = 200

可以看出,两个线程中分别对私有键值进行了初始化、读取、修改、读取的过程,所得到的键值按照预期的规律发生了变化,同时又彼此独立互不影响。

三、补充说明

这里要说明一下,不少CSDN网友所转载的文章里,都对上面的代码使用“gcc -lpthread test.c -o test”这样的语句进行编译链接,但是却没人发现这里面的问题!!

大家可以自己去编译一下试试看,如果你输入“gcc -lpthread test.c -o test”,绝对是编译不过的,系统会提示:

/tmp/ccSkayWg.o:在函数‘test’中:
thread_spc.c:(.text+0x11):对‘pthread_getspecific’未定义的引用
/tmp/ccSkayWg.o:在函数‘thread_func’中:
thread_spc.c:(.text+0x54):对‘pthread_setspecific’未定义的引用
thread_spc.c:(.text+0x61):对‘pthread_getspecific’未定义的引用
/tmp/ccSkayWg.o:在函数‘main’中:
thread_spc.c:(.text+0xd0):对‘pthread_key_create’未定义的引用
thread_spc.c:(.text+0xed):对‘pthread_create’未定义的引用
thread_spc.c:(.text+0x10a):对‘pthread_create’未定义的引用
thread_spc.c:(.text+0x11b):对‘pthread_join’未定义的引用
thread_spc.c:(.text+0x12c):对‘pthread_join’未定义的引用

这是因为gcc依赖顺序的问题导致的。就是gcc编译的时候,各个文件的依赖顺序是有讲究的。如果文件a依赖于文件b,那么编译的时候必须把a放前面,b放后面。

例如,在main.c中使用了pthread库相关函数,那么编译的时候必须是main.c在前,-lpthread在后:

gcc main.c -lpthread -o a.out

所以,上面出现问题的原因就是引入库的顺序出了问题,不能放在前面,得放到后面去:

gcc test.c -lpthread -o test
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值