每线程变量,官方叫法是:Thread-local storage (TLS)。这种变量每个线程都会有一个实例,很多场景需要每线程变量,最典型的就是errno。
每线程变量,使用__thread关键字定义,这个关键字不是c语言标准定义的,而是业界(编译器开发者)根据实际需要扩展的。
下面例子,主线程和子线程,变量a的地址不同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | $ cat test.c #include <pthread.h> #include <stdio.h> static __thread int a; static void *start_routine(void *arg) { printf("child thread %p\n", &a); } int main(int argc, char **argv) { pthread_t tid; printf("main thread %p\n", &a); pthread_create(&tid, NULL, start_routine, NULL); pthread_join(tid, NULL); return 0; } $ cc test.c -lpthread $ ./a.out main thread 0x7f438288b73c child thread 0x7f438288a6fc |
__thread的支持,不仅需要编译器(gcc),还需要linker (ld),dynamic linker loader (ld.so),系统库(libc.so libpthread.so)的支持。而且需要elf格式的扩展支持(见参考2)
对每线程变量取地址,并共享给其它线程是合法的,但是地址的生命周期,在线程结束后也跟着结束了。
在x64系统上,没线程变量,依赖于segment register实现,
比如:
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 | $ cat test.c static __thread int a; void *addr() { return &a; } $ cc -S test.c -Os $ cat test.s .file "test.c" .text .globl addr .type addr, @function addr: .LFB0: .cfi_startproc endbr64 movq %fs:0, %rax addq $a@tpoff, %rax ret .cfi_endproc .LFE0: .size addr, .-addr .section .tbss,"awT",@nobits .align 4 .type a, @object .size a, 4 a: .zero 4 .ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4: |
可以看到访问a,使用了%fs寄存器
参考:
GCC manual. 6.68 Thread-Local Storage
https://gcc.gnu.org/onlinedocs/gcc/Thread-Local.html
ELF Handling For Thread-Local Storage
https://www.akkadia.org/drepper/tls.pdf
https://stackoverflow.com/questions/10810203/what-is-the-fs-gs-register-intended-for