pthread_once函数
前些天看adb的源代码,发现很多地方用了
pthread_once
去做相应模块的初始化,就去研究了下这个函数。
需求
在使用任何的模块,编写代码一个很常见的步骤就是:
- 初始化
- 中间操作、工作代码
- 销毁、释放
文件操作,数据库操作、va_list、cJSON、日志框架基本都是这样。现在关注点放在上面的第一步在,初始化有几种常见的处理方式:
- 显示调用初始化
- 隐式调用初始化:
- 在使用时,用标记记录有没有初始化
pthread_once
函数- 编译器的
constructor
、destructor
属性
显示调用初始化
这个不用说了,一般模块、框架之类的,还是建议显示的调用去初始化。另外一些常用的,类似工具类的,可以使用隐式初始化,比如Android的SharedPreferences、 malloc的初始化
用标记记录有没有初始化
如下代码,用random_is_initialized
记录有没有初始化,没有初始化就去执行下对应的初始化代码
#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 函数
函数原型:
#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_once
和pthread_create
不一样,pthread_once
会等待线程的函数执行完才会返回。上面的代码就可以改成:
#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;
}
但我又有了第二个疑问
线程会开辟一个函数调用栈,会不会太浪费?
新开线程会去分配一个默认大小的空间,来去管理函数的调用(先进后出)。这个函数只执行一个函数,会不会太浪费空间。当然,这一个函数下面会执行很多的函数,我的意思是相对于主程序,函数的调用还是相对较少。我的电脑默认栈空间大小:
$ 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
。代码如下:
#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
#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-函数
编译器的constructor
、destructor
属性
这个不重复了,之前写的文章:https://wshibin.github.io/2022/08/Compiler-Attributes/#constructor、destructor-属性
总结
pthread_once
就是一典型场景需求推动(模块初始化),而诞生的函数功能,封装下,简化并对其进行了优化。