提问人:darune 提问时间:4/3/2019 最后编辑:Bret Copelanddarune 更新时间:2/14/2022 访问量:18295
std::call_once,什么时候应该使用?
std::call_once, when should it be used?
问:
C++11 中引入的 std::call_once
函数可确保以线程安全的方式精确调用可调用对象一次。
由于这可以通过其他方式实现 - 什么时候应该使用?它旨在解决什么类型的问题?std::call_once
请举例说明。
答:
示例:我将其用于 libcURL 以从网站检索 http(s) 数据。在 libcURL 中,必须先进行一次性全局初始化,然后才能使用该库。鉴于初始化不是线程安全的,但从网站请求数据是线程安全的,我使用它只调用我的初始化一次,无论在哪个线程中以及它是否同时调用。call_once
评论
static
std::call_once
std::call_one
想象一下,一个包含一些巨大数据的单例实例(出于某种原因):
class Singleton {
public: static Singleton& get();
...
private: static std::unique_ptr<SingletonDataBase> instance;
}
我们如何确保 get 函数在正确调用时创建实例(无论出于何种原因,该实例非常大,不能进入静态内存空间)。我们如何实现这一目标?
- 使用 ?我猜有点丑。
mutex
- 用?Nicer,并坚定地给出了代码的意图:
std::call_once
Singleton& Singleton::get() {
static std::once_flag flag;
std::call_once(flag, [&](){ instance.reset(new SingletonDataBase()); });
return instance.get_interface()
}
每当您需要调用一次时,它都很好用。call_once
评论
static
which for whatever reason is really large and can't go in static memory space
什么时候应该使用?
当您想调用某物一次时。它简明扼要地说明了它在做什么。
替代方案
struct CallFooOnce {
CallFooOnce() {
foo();
}
};
static CallFooOnce foo_once;
有更多的样板,并引入了一个额外的名称,超过
static std::once_flag foo_once;
std::call_once(foo_once, foo);
评论
foo_once
也是一个附加名称。此外,您不太可能只有一个要调用的可调用对象。通常你想把它包装在一些错误处理代码中,你也可以把它放在承包商中。在这一点上,样板的数量没有太大区别。
foo_once
CallFooOnce
std::call_once(foo_once, foo);
foo()
foo()
std::call_once
foo
foo
std::call_once
do_thing()
get()
典型用途是当您希望在可能争用(多线程)的情况下按需初始化全局数据片段时。
假设你有结构
struct A{ A() {/*do some stuff*/} };
并且您希望在全局范围内使用它的实例。
如果按照以下方式执行操作,它将在 main 之前初始化,因此它不是按需的。
A a_global;
如果按照以下方式执行操作,则它是按需的,但不是线程安全的。
A *a_singleton = NULL;
A *getA() {
if (!a_singleton)
a_singleton = new A();
return a_singleton;
}
call_once
解决了这两个问题。当然,您可以改用其他同步原语的某种组合,但您最终只会重新实现自己的 .call_once
评论
A *getA() { static A* a_singleton = new A(); return a_singleton; }
缓存和延迟评估。假设一个不可变类具有存储成本低但计算成本高的属性 。与其按需计算或预先计算,不如这样做double foo() const;
private:
mutable std::once_flag m_flag;
mutable double m_foo;
double doCalcFoo() const; // Expensive!
public:
double foo() const {
std::call_once(m_flag, [this] { m_foo = doCalcFoo(); });
return m_foo;
}
虽然你可以做
private:
mutable std::optional<double> m_foo;
mutable std::mutex m_fooMutex;
double doCalcFoo() const; // Expensive!
public:
double foo() const {
std::lock_guard lock{m_fooMutex};
if (!m_foo) {
m_foo = doCalcFoo();
}
return *m_foo;
}
这更多的字节(40 + 16 = 56 字节,而 Clang 上为 4 + 8 + 填充 = 16),性能优化程度较低,并且违反了 Parent 的“无原始同步基元”的 Better-Code 目标:(https://sean-parent.stlab.cc/presentations/2016-08-08-concurrency/2016-08-08-concurrency.pdf 第 6 张幻灯片到第 11 张幻灯片)。
评论
const
const
protected: void resetFoo() { m_flag.~once_flag(); new (&m_flag) std::once_flag{}; } to reset the flag in a concurrency-unsafe way. I think that makes calling
int
std::call_once()
可以用于延迟计算,即使许多线程执行传递给它的可调用对象也只会执行一次。请参阅下一个示例,其中该方法可能被许多线程多次调用,但是该实例仅创建一次,并且需要它(首次调用 getInstance())。getInstance()
#include <mutex>
class Singleton {
static Singleton *instance;
static std::once_flag inited;
Singleton() {...} //private
public:
static Singleton *getInstance() {
std::call_once( inited, []() {
instance=new Singleton();
});
return instance;
}
};
评论
其他已经提到的答案,效果可以通过魔术静电或使用来实现。call_once
mutex
我想专注于性能和极端情况处理。
使用互斥锁
会比在快速路径上慢(当目标函数已被调用时)。这是因为获取/释放将是原子 RMW,而查询只是读取。您可以通过实现双重检查锁定来修复性能,但这会使程序复杂化。call_once
mutex
call_once
mutex
如果首先尝试仅在程序退出时调用目标函数,则使用 call_once
也比互斥锁更安全。 不一定是可轻易破坏的,因此全局互斥锁可能会在调用目标时被销毁。mutex
Magic static 也可能在快速路径上快速运行,只需读取以查询对象是否已初始化。magic static 与 call_once
的性能可能取决于实现,但通常 magic static 有更多的优化机会,因为它不必在变量中公开状态,并且必须由编译器实现,而不仅仅是在库中实现。
特定于 MSVC:magic statics 采用线程本地存储。这比 更优化。但是,可以通过 /Zc:threadSafeInit- 关闭神奇的静态支持。关闭它的能力是由于一些线程本地存储的缺点。 无法关闭。在 Visual Studio 2015 之前,没有很好地实现,并且根本没有实现魔术静态,因此如果没有第三方库,很难获得一次性初始化。call_once
call_once
call_once
评论