首页 > 代码库 > fork安全的gettid高效实现
fork安全的gettid高效实现
进程有id,可以通过getpid()获得,线程也有id,但是glibc没有提供封装。需要自己发出系统调用。在关键路径,系统调用还是对性能有影响的。因此我们可以想到类似glibc对getpid做的cache化封装,用thread local的方式缓存每个线程的id,每个线程只有第一次调用gettid时才真正发起系统调用。
#include <stdio.h>#include <syscall.h>#include <unistd.h>pid_t gettid() { static __thread pid_t cached_tid; if (cached_tid == 0) { cached_tid = syscall(SYS_gettid); } return cached_tid;}
这段代码运行的很好,直到遇到fork。在我看来,fork是单线程时代的东西,与多线程格格不入,所以我们的代码中很少用到。其实这个问题除了对调用fork的线程来说是诡异的,因为fork时,其他线程是不会被fork的。但是对于主线程或者单线程程序,这个问题也还是存在的,存在就不爽。
或许可以想到用pthread_atfork来做这个事情,其原型是这样的。
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
文档说,child会在子进程中被调用。但是这里有个问题,cached_tid是线程局部变量,每个线程里的地址是不一样的,而child函数不支持传地址,难道我们要用动态生成代码(thunk)的猥琐方式?
其实不必那么复杂,虽然glibc缓存了getpid,但是fork后getpid是会变的,因此我们可以在每次调用时再多比较一下pid是否发生了变化:
pid_t gettid() { static __thread pid_t cached_pid; static __thread pid_t cached_tid; pid_t pid = getpid(); if (cached_pid != pid || cached_tid == 0) { cached_pid == pid; cached_tid = syscall(SYS_gettid); } return cached_tid;}
getpid是高效的,因此这段代码也是高校的。
查阅glibc的源代码,getpid是从当前线程控制块里读取的,其实里面也有线程id,但是glibc却迟迟不肯提供封装,让我想起了一句话,程序员何苦为难程序员。
测一下:
int main() { printf("Parent: pid=%d, tid=%d\n", getpid(), gettid()); if (fork() == 0) { printf("Child: pid=%d, tid=%d\n", getpid(), gettid()); } else { printf("Parent after fork: pid=%d, tid=%d\n", getpid(), gettid()); }}
Parent: pid=10776, tid=10776
Parent after fork: pid=10776, tid=10776
Child: pid=10777, tid=10777
行为符合预期。
fork安全的gettid高效实现