Skip to content
Published at:

pthread_once函数

前些天看adb的源代码,发现很多地方用了pthread_once去做相应模块的初始化,就去研究了下这个函数。

需求

在使用任何的模块,编写代码一个很常见的步骤就是:

  1. 初始化
  2. 中间操作、工作代码
  3. 销毁、释放

文件操作,数据库操作、va_list、cJSON、日志框架基本都是这样。现在关注点放在上面的第一步在,初始化有几种常见的处理方式:

  • 显示调用初始化
  • 隐式调用初始化:
    • 在使用时,用标记记录有没有初始化
    • pthread_once 函数
    • 编译器的constructordestructor属性

显示调用初始化

这个不用说了,一般模块、框架之类的,还是建议显示的调用去初始化。另外一些常用的,类似工具类的,可以使用隐式初始化,比如Android的SharedPreferences、 malloc的初始化

用标记记录有没有初始化

如下代码,用random_is_initialized记录有没有初始化,没有初始化就去执行下对应的初始化代码

c
#include <stdio.h>

static int random_is_initialized = 0;
extern int initialize_random();

int random_function() {
    if (random_is_initialized == 0) {
        initialize_random();
        random_is_initialized = 1;
    }
    // ... /* Operations performed after initialization. */
}

int main(int argc, char* argv[]) {
    random_function();
    random_function();、
    // ...
    return 0;
}

pthread_once 函数

函数原型:

c
#include <pthread.h>

pthread_once_t once_control = PTHREAD_ONCE_INIT;

int pthread_once(pthread_once_t* once_control, void (*init_routine)(void));

三部分组成:

  • pthread_once_t结构体
  • PTHREAD_ONCE_INIT 初始化宏
  • pthread_once函数,成功返回0,失败返回对应的错误码

我最开始见到这个函数时,函数名phtread_once上带着pthread。有了我第一个疑问,

开线程做初始化如何保证先后循序?

然而,看完pthrea_once的文档之后,发现pthrea_oncepthread_create不一样,pthread_once等待线程的函数执行完才会返回。上面的代码就可以改成:

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

static pthread_once_t random_is_initialized = PTHREAD_ONCE_INIT;
extern int initialize_random();

int random_function() {
    (void)pthread_once(&random_is_initialized, initialize_random);
    // ... /* Operations performed after initialization. */
}

int main(int argc, char* argv[]) {
    random_function();
    random_function();
    return 0;
}

但我又有了第二个疑问

线程会开辟一个函数调用栈,会不会太浪费?

新开线程会去分配一个默认大小的空间,来去管理函数的调用(先进后出)。这个函数只执行一个函数,会不会太浪费空间。当然,这一个函数下面会执行很多的函数,我的意思是相对于主程序,函数的调用还是相对较少。我的电脑默认栈空间大小:

bash
$ ulimit -a
-t: cpu time (seconds)              unlimited
-f: file size (blocks)              unlimited
-d: data seg size (kbytes)          unlimited
-s: stack size (kbytes)             8192
-c: core file size (blocks)         0
-v: address space (kbytes)          unlimited
-l: locked-in-memory size (kbytes)  unlimited
-u: processes                       5568
-n: file descriptors                256

$ ulimit -s
8192
#define __PTHREAD_SIZE__            8176    // pthread_t默认栈大小
#define __PTHREAD_ATTR_SIZE__       56
#define __PTHREAD_MUTEXATTR_SIZE__  8
#define __PTHREAD_MUTEX_SIZE__      56
#define __PTHREAD_CONDATTR_SIZE__   8
#define __PTHREAD_COND_SIZE__       40
#define __PTHREAD_ONCE_SIZE__       8       // pthread_once_t的栈大小
#define __PTHREAD_RWLOCK_SIZE__     192
#define __PTHREAD_RWLOCKATTR_SIZE__ 16

最后还有一个问题

init_routine在整个程序生命周期只执行了一只,怎么优化?

pthread_once函数的第二个参数init_routine函数指针,在整个程序的生命周期,只被执行了一次;问题是,函数每次都会执行到pthread_once,会去判断我这个init_routine函数指针有没有执行过;换种方式说,pthread_once函数内部一定有一个if-else的判断,判断这个init_routine函数指针有没有执行过,那么这个if-else如何去优化。

简化版本是这样:一个while死循环,里面有一个if-else的分支,if-else分支里面false的情况只执行过一次,其它情况都是true的情况,如何优化这个if-else。代码如下:

c
#include <stdio.h>
#include <stdbool.h>

static bool cond = false;

int main(int argc, char* argv[]) {
    // ...
    while (true) {
        if (cond) {
            // handle ...
        } else {
            // init ...
            cond = true;
        }
    }
    return 0;
}

其实在语言层面上已经很难去做了,答案是:编译器的分支预测优化__builtin_expect函数 。下面是pthread_once代码:

源码链接:https://codebrowser.dev/glibc/glibc/nptl/pthread_once.c.html#___pthread_once

c
#if (__GNUC__ >= 3) || __glibc_has_builtin(__builtin_expect)
#define __glibc_unlikely(cond) __builtin_expect((cond), 0)
#define __glibc_likely(cond) __builtin_expect((cond), 1)
#else
#define __glibc_unlikely(cond) (cond)
#define __glibc_likely(cond) (cond)
#endif

int ___pthread_once(pthread_once_t* once_control, void (*init_routine)(void)) {
    /* Fast path.  See __pthread_once_slow.  */
    int val;
    val = atomic_load_acquire(once_control);
    if (__glibc_likely((val & __PTHREAD_ONCE_DONE) != 0))
        return 0;
    else
        return __pthread_once_slow(once_control, init_routine);
}

GNU GCC文档:https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html 之前写的文章:https://wshibin.github.io/2022/08/Compiler-Attributes/#builtin-expect-函数

编译器的constructordestructor属性

这个不重复了,之前写的文章:https://wshibin.github.io/2022/08/Compiler-Attributes/#constructor、destructor-属性

总结

pthread_once就是一典型场景需求推动(模块初始化),而诞生的函数功能,封装下,简化并对其进行了优化。

Updated at: