基础知识
在多线程的程序中,进程中的全局变量与函数内定义的静态(static)变量,是各个线程都可以访问的共享变量,因此往往会有资源条件竞争的问题,常见的处理办法就是使用同步机制来维护资源
线程局部存储(Thread Local Storage,TLS)是一种另类的解决方式:
它存储和维护一些线程相关的数据,存储的数据会被关联到当前线程中去,并不需要锁来维护
本质上是为每一个使用该全局变量的线程都提供一个变量值的副本,每一个线程均可以独立地改变自己的副本,而不会和其它线程的副本冲突
TLS实现方式:API
Linux 中相关的 API:
1 2 3 4 int pthread_key_create (pthread_key_t *key, void (*destructor)(void *)) ; int pthread_key_delete (pthread_key_t key) ; void *pthread_getspecific (pthread_key_t key) ; int pthread_setspecific (pthread_key_t key, const void *value) ;
本人在实际操作这些 API 时遇到了许多问题,先看一个失败的案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <stdio.h> #include <pthread.h> #include <string.h> pthread_key_t p_key; void *thread_func (void *args) { printf ("%d is runing in %s\n" ,*(int *)args,__func__); *(int *)args = *(int *)args + 1 ; printf ("%d is runing in %s\n" ,*(int *)args,__func__); return (void *)0 ; } int main () { pthread_t p[24 ]; pthread_key_create(&p_key,NULL ); int *a = (int *)pthread_getspecific(p_key); a = malloc (sizeof (int )); pthread_setspecific(p_key, a); *a = 1 ; for (int i=0 ;i<24 ;i++){ pthread_create(&p[i], NULL ,thread_func,a); } return 0 ; }
1 2 3 4 5 6 7 8 9 ➜ exp ./test 1 is runing in thread_func2 is runing in thread_func1 is runing in thread_func2 is runing in thread_func3 is runing in thread_func5 is runing in thread_func3 is runing in thread_func......
位于堆区的 a
本来应该充当 TLS 的作用,但各个线程好像还是共用了同一片空间
pthread_setspecific
对主线程进行了绑定,而忽略了其他线程(其实我还有多个测试案例:把 pthread_setspecific
放到线程函数里面,使用不同的 pthread_key_t
变量 …… 但这些操作都失败了)
其实这里是我的理解有误区,认为这些 API 函数会自动帮我为不同的线程复制数据
接下来看一个成功的案例:(错误码 errno 的实现原理)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <pthread.h> void *thread_deal_func (void *arg) ; #define TASK_NUM 2 pthread_t global_thread_no[TASK_NUM]; static pthread_key_t key;static pthread_once_t key_once = PTHREAD_ONCE_INIT;static void make_key () { (void ) pthread_key_create(&key, NULL ); } int * _errno(){ int *ptr; (void ) pthread_once(&key_once, make_key); if ((ptr = pthread_getspecific(key)) == NULL ) { ptr = malloc (sizeof (int )); (void ) pthread_setspecific(key, ptr); } return ptr ; } #define errno_test *_errno() int main () { errno_test = 100 ; int tmp = 0 ,i = 0 ; for (i = 0 ;i < TASK_NUM; i++){ if ((tmp=pthread_create(&global_thread_no[i],NULL ,thread_deal_func,&i))!= 0 ){ printf ("can't create thread: %s\n" ,strerror(tmp)); return -1 ; } } while (1 ){ printf ("man thread ,errno_test:%d\n" ,errno_test); sleep(1 ); } return 0 ; } void *thread_deal_func (void *arg) { int number = *(int *)arg; while (1 ){ errno_test += 1 ; printf ("thread number:%d,errno_test:%d\n" ,number,errno_test); sleep(1 ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 ➜ exp ./test man thread ,errno_test:100 thread number:1 ,errno_test:1 thread number:2 ,errno_test:1 man thread ,errno_test:100 thread number:2 ,errno_test:2 thread number:1 ,errno_test:2 thread number:2 ,errno_test:3 thread number:1 ,errno_test:3 man thread ,errno_test:100 thread number:2 ,errno_test:4 man thread ,errno_test:100 thread number:1 ,errno_test:4
1 2 3 4 5 6 7 8 9 10 ➜ exp ./test thread number:1 ,errno_test:101 man thread ,errno_test:101 thread number:2 ,errno_test:102 thread number:2 ,errno_test:103 man thread ,errno_test:103 thread number:1 ,errno_test:104 thread number:2 ,errno_test:105 thread number:1 ,errno_test:106 man thread ,errno_test:106
这里的 errno_test
看起来像是全局变量,其实是一个函数的返回值
1 #define errno_test *_errno()
每次使用 errno_test
时,都会调用 _errno()
函数
在其中会申请与当前线程绑定的变量,并且调用 malloc
为其分配空间:
对于各个线程来说,这些变量的地址不同了
对于用户来说,各个线程拿到不同的变量,不会相互干扰
TLS实现方式:关键字
在 Linux 中还有一种更为高效的线程局部存储方法,就是使用关键字 __thread
来定义变量
凡是带有 __thread
的变量,每个线程都拥有该变量的一份拷贝,且互不干扰
线程局部存储中的变量将一直存在,直至线程终止,当线程终止时会自动释放这一存储
测试案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <stdio.h> #include <stdlib.h> #include <pthread.h> __thread int var = 0 ; void * task_entry (void * arg) { int idx = (int )arg; int i; printf ("addr : %p\n" ,&var); for (i = 0 ; i < 5 ; ++i) { printf ("thread:%d var = %d\n" , idx, var += idx); sleep(1 ); } } int main () { pthread_t pid1,pid2; pthread_create(&pid1,NULL ,task_entry,(void *)1 ); pthread_create(&pid2,NULL ,task_entry,(void *)2 ); pthread_join(pid1,NULL ); pthread_join(pid2,NULL ); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 ➜ exp ./test addr : 0x7f20b3ebe6fc thread:1 var = 1 addr : 0x7f20b36bd6fc thread:2 var = 2 thread:1 var = 2 thread:2 var = 4 thread:2 var = 6 thread:1 var = 3 thread:1 var = 4 thread:2 var = 8 thread:2 var = 10 thread:1 var = 5
现在我们就分析一下 __thread
底层的逻辑,测试案例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <stdio.h> #include <stdlib.h> #include <pthread.h> __thread size_t name1 = 0x111111 ; __thread size_t name2 = 0x222222 ; size_t name3 = 0x333333 ;size_t name4 = 0x444444 ;__thread size_t name5 = 0x555555 ; __thread size_t name6 = 0x666666 ; void * task_entry1 () { printf ("addr:0x%lx\n" ,name1); name1 = 0x777777 ; while (1 ){} } void * task_entry2 () { printf ("addr:0x%lx\n" ,name2); name1 = 0x888888 ; while (1 ){} } int main () { pthread_t pid1,pid2; pthread_create(&pid1,NULL ,task_entry1,NULL ); pthread_create(&pid2,NULL ,task_entry2,NULL ); pthread_join(pid1,NULL ); pthread_join(pid2,NULL ); return 0 ; }
打开 GDB 进行调试:
在 GDB 中输入 tls
可以显示 TLS 的位置(在 ubuntu 中 TLS 存储在 FS 寄存器中)
1 2 pwndbg> tls tls : 0x7ffff7d99740
而之前添加了 __thread
关键字的变量则存储在 TLS 的上方
1 2 3 4 5 6 7 8 9 pwndbg> telescope 0x7ffff7d99740 -0x20 00 :0000 │ 0x7ffff7d99720 ◂— 0x111111 01 :0008 │ 0x7ffff7d99728 ◂— 0x222222 02 :0010 │ 0x7ffff7d99730 ◂— 0x555555 03 :0018 │ 0x7ffff7d99738 ◂— 0x666666 04 :0020 │ 0x7ffff7d99740 ◂— 0x7ffff7d99740 05 :0028 │ 0x7ffff7d99748 —▸ 0x7ffff7d9a0a0 ◂— 0x1 06 :0030 │ 0x7ffff7d99750 —▸ 0x7ffff7d99740 ◂— 0x7ffff7d99740 07 :0038 │ 0x7ffff7d99758 ◂— 0x1
函数 pthread_create
会在 heap 上申请一片区域:
1 2 3 4 5 6 7 Allocated chunk | PREV_INUSE Addr: 0x555555559290 Size: 0x131 Allocated chunk | PREV_INUSE Addr: 0x5555555593c0 Size: 0x131
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pwndbg> telescope 0x555555559290 00 :0000 │ 0x555555559290 ◂— 0x0 01 :0008 │ 0x555555559298 ◂— 0x131 02 :0010 │ 0x5555555592a0 ◂— 0x10 03 :0018 │ 0x5555555592a8 ◂— 0x0 04 :0020 │ 0x5555555592b0 ◂— 0x1 05 :0028 │ 0x5555555592b8 ◂— 0x0 06 :0030 │ 0x5555555592c0 —▸ 0x7ffff7d986e0 ◂— 0x777777 07 :0038 │ 0x5555555592c8 ◂— 0x0 08 :0040 │ 0x5555555592d0 —▸ 0x7ffff7d98650 —▸ 0x7ffff7f894a0 (_nl_global_locale) —▸ 0x7ffff7f856c0 (_nl_C_LC_CTYPE) —▸ 0x7ffff7f51fd9 (_nl_C_name) ◂— ...09 :0048 │ 0x5555555592d8 ◂— 0x0 pwndbg> telescope 0x7ffff7d986e0 00 :0000 │ 0x7ffff7d986e0 ◂— 0x777777 01 :0008 │ 0x7ffff7d986e8 ◂— 0x222222 02 :0010 │ 0x7ffff7d986f0 ◂— 0x555555 03 :0018 │ 0x7ffff7d986f8 ◂— 0x666666 04 :0020 │ r15 0x7ffff7d98700 ◂— 0x7ffff7d98700 05 :0028 │ 0x7ffff7d98708 —▸ 0x5555555592b0 ◂— 0x1 06 :0030 │ 0x7ffff7d98710 —▸ 0x7ffff7d98700 ◂— 0x7ffff7d98700 07 :0038 │ 0x7ffff7d98718 ◂— 0x1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pwndbg> telescope 0x5555555593c0 00 :0000 │ 0x5555555593c0 ◂— 0x0 01 :0008 │ 0x5555555593c8 ◂— 0x131 02 :0010 │ 0x5555555593d0 ◂— 0x10 03 :0018 │ 0x5555555593d8 ◂— 0x0 04 :0020 │ 0x5555555593e0 ◂— 0x1 05 :0028 │ 0x5555555593e8 ◂— 0x0 06 :0030 │ 0x5555555593f0 —▸ 0x7ffff75976e0 ◂— 0x888888 07 :0038 │ 0x5555555593f8 ◂— 0x0 08 :0040 │ 0x555555559400 —▸ 0x7ffff7597650 —▸ 0x7ffff7f894a0 (_nl_global_locale) —▸ 0x7ffff7f856c0 (_nl_C_LC_CTYPE) —▸ 0x7ffff7f51fd9 (_nl_C_name) ◂— ...09 :0048 │ 0x555555559408 ◂— 0x0 pwndbg> telescope 0x7ffff75976e0 00 :0000 │ 0x7ffff75976e0 ◂— 0x888888 01 :0008 │ 0x7ffff75976e8 ◂— 0x222222 02 :0010 │ 0x7ffff75976f0 ◂— 0x555555 03 :0018 │ 0x7ffff75976f8 ◂— 0x666666 04 :0020 │ r9 0x7ffff7597700 ◂— 0x7ffff7597700 05 :0028 │ 0x7ffff7597708 —▸ 0x5555555593e0 ◂— 0x1 06 :0030 │ 0x7ffff7597710 —▸ 0x7ffff7597700 ◂— 0x7ffff7597700 07 :0038 │ 0x7ffff7597718 ◂— 0x1
其实看到这里就很清晰了
函数 pthread_create
在调用时会把主线程 TLS 上方存储 __thread
变量的内存空间给复制一份,添加到自己的 TLS 上方
然后申请一片 heap 空间来记录数据
如果 pthread_create
生成的线程想要写入带有 __thread
关键字的变量,就会操作自己 TLS 上方所记录的变量,而不会影响到其他线程