如果您有兼容 c++11 的编译器
(例如,不是 VS2013)
两者,最简单和最有效的方法就是写:
int f(int x) {
static int firstArg = x;
return firstArg*x;
}
c++11 标准要求函数局部静态变量的初始化是线程安全的 *)。更准确地说,它要求只有一个线程初始化变量,并且所有其他线程都等待,直到初始化完成(稍后的读写当然仍然可以竞争,但因为这是对firstArg的唯一写访问,这里不需要额外的同步)。
如果您的编译器不支持“magic statics”
下一个最佳方法是使用 Sebastian Redl 建议的 std::call_once,它具有相同的语义。
如果通过std::call_once 初始化太慢(它可能使用互斥体)并且arg 是内置类型(如int),您可以尝试以下方法(我没有做任何测量):
namespace {
const int DISALLOWED_VALUE = std::numeric_limits<int>::max();
std::atomic<int> firstArg= DISALLOWED_VALUE;
}
int f(int x) {
if (firstArg.load(std::memory_order_relaxed) == DISALLOWED_VALUE) {
int tmp = DISALLOWED_VALUE;
firstArg.compare_exchange_strong(tmp, x);
}
return firstArg.load(std::memory_order_relaxed)*x;
}
DISALLOWED_VALUE 是一些不可能作为有效参数传递给 f 的值。在这种情况下,std::numeric_limits<int>::max() 与自身相乘时会导致整数溢出,因此它不是f 的有效参数,因此可以作为firstArg 尚未初始化的指标。
警告: 仅在您已验证 std::call_once 对于您的特定工作负载(几乎永远不会出现这种情况)的速度慢得无法接受并且此版本实际上已经足够改进时才使用此版本。
条件锁定注意事项
由于有一些答案提出了各种错误的条件锁定算法,我还提出了双重检查锁定的正确手动实现。
namespace {
std::atomic<bool> isInit = false; //has to be atomic
std::mutex mux;
}
int f(int x) {
static int firstArg;
if (!isInit.load(std::memory_order_acquire)) {
std::lock_guard<std::mutex> lg(mux);
if (!isInit.load(std::memory_order_acquire)) {
firstArg = x;
isInit.store(true,std::memory_order_release);
}
}
return firstArg*x;
}
两个重要的部分是:
- 进行双重检查(一次在受保护区域内,一次在受保护区域外)
- 使用
std::atomic 作为标志。否则,不能保证未锁定的线程观察到标志和变量的存储顺序。
致谢:
双重检查锁定版本基于 Herb Sutter 在 cppcon2014 上的演示,并根据 EOF 和 Sebastian 的 cmets/answers 进行了扩充。
*) 参见例如this question 以及来自 c++14 标准的最新工作草案(6.7 第 4 点):
如果在初始化变量时控制同时进入声明,则并发执行将等待初始化完成。