前言
本文的分析基于llvm的libc++,而不是gun的libstdc++,因为libstdc++的代码里太多宏了,看起来蛋疼。
在多线程编程中,有一个常见的情景是某个任务只需要执行一次。在C++11中提供了很方便的辅助类once_flag,call_once。
声明
首先来看一下once_flag和call_once的声明:
1 | struct once_flag |
可以看到once_flag是不允许修改的,拷贝构造函数和operator=函数都声明为delete,这样防止程序员乱用。
另外,call_once也是很简单的,只要传进一个once_flag,回调函数,和参数列表就可以了。
示例
看一个示例:
http://en.cppreference.com/w/cpp/thread/call_once
1 |
|
保存为main.cpp,如果是用g++或者clang++来编绎:
1 | g++ -std=c++11 -pthread main.cpp |
可以看到,只会输出一行
1 | Called once |
值得注意的是,如果在函数执行中抛出了异常,那么会有另一个在once_flag上等待的线程会执行。
比如下面的例子:
1 |
|
输出的结果可能是0到3行throw,和一行once。
实际上once_flag相当于一个锁,使用它的线程都会在上面等待,只有一个线程允许执行。如果该线程抛出异常,那么从等待中的线程中选择一个,重复上面的流程。
实现分析
once_flag实际上只有一个unsigned long state_的成员变量,把call_once声明为友元函数,这样call_once能修改state__变量:
1 | struct once_flag |
call_once则用了一个__call_once_param类来包装函数,很常见的模板编程技巧。
1 | template <class _Fp> |
最重要的是__call_once函数的实现:
1 | static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER; |
里面用了全局的mutex和condition来做同步,还有异常处理的代码。
其实当看到mutext和condition时,就明白是如何实现的了。里面有一系列的同步操作,可以参考另外一篇blog:
- http://blog.csdn.net/hengyunabc/article/details/27969613 并行编程之条件变量(posix condition variables)
尽管代码看起来很简单,但是要仔细分析它的各种时序也比较复杂。
有个地方比较疑惑的:
- 对于同步的state变量,并没有任何的memory order的保护,会不会有问题?
因为在JDK的代码里LockSupport和逻辑和上面的__call_once函数类似,但是却有memory order相关的代码:
OrderAccess::fence();
其它的东东
有个东东值得提一下,在C++中,static变量的初始化,并不是线程安全的。
比如
1 | void func(){ |
实际上相当于这样的代码:
1 | int __flag = 0 |
总结
还有一件事情要考虑:所有的once_flag和call_once都共用全局的mutex和condition会不会有性能问题?
首先,像call_once这样的需求在一个程序里不会太多。另外,临界区的代码是比较很少的,只有判断各自的flag的代码。
如果有上百上千个线程在等待once_flag,那么pthread_cond_broadcast可能会造成“惊群”效果,但是如果有那么多的线程都上等待,显然程序设计有问题。
还有一个要注意的地方是 once_flag的生命周期,它必须要比使用它的线程的生命周期要长。所以通常定义成全局变量比较好。