首页 > 代码库 > 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高效实现