std::call_once,什么时候应该使用?

std::call_once, when should it be used?

提问人:darune 提问时间:4/3/2019 最后编辑:Bret Copelanddarune 更新时间:2/14/2022 访问量:18295

问:

C++11 中引入的 std::call_once 函数可确保以线程安全的方式精确调用可调用对象一次。

由于这可以通过其他方式实现 - 什么时候应该使用?它旨在解决什么类型的问题?std::call_once

请举例说明。

C++ C++11 并发

评论

0赞 yuri kilochek 4/3/2019
我不知道 [可能是局部] 静态对象构造函数不会更好地服务于任何情况。
0赞 Fantastic Mr Fox 4/3/2019
@yurikilochek 如果您希望它在全局范围内,但不在 main 之前构造,该怎么办?
0赞 yuri kilochek 4/3/2019
@FantasticMrFox这就是我所说的可能是本地的意思。把它放在一个函数中,当控制流第一次到达它时,它将被初始化。
1赞 einpoklum 11/2/2020
@darune,我相信你接受的答案是不正确的 - 请考虑不接受它。
1赞 einpoklum 11/5/2020
@darune:确实,你可以用两种方式做到这一点,但公认的答案是不正确的,因为静态初始化不是线程安全的(这是它的重点)。所以现在,我们正在告诉人们一些误导性的东西。

答:

32赞 The Quantum Physicist 4/3/2019 #1

示例:我将其用于 libcURL 以从网站检索 http(s) 数据。在 libcURL 中,必须先进行一次性全局初始化,然后才能使用该库。鉴于初始化不是线程安全的,但从网站请求数据是线程安全的,我使用它只调用我的初始化一次,无论在哪个线程中以及它是否同时调用。call_once

评论

1赞 einpoklum 11/2/2020
一次性全局初始化需要 std::call_once 是不正确的。C++11 要求静态存储对象可以安全地初始化。有关编译器如何安排发生这种情况的示例,请参阅此帖子。不需要 std::call_once。
3赞 The Quantum Physicist 11/4/2020
在这种情况下,@einpoklum 和 是不一样的。 具有由开发人员控制的状态。在我的示例中,您可能有许多不同的位置必须调用初始化,但只能由想要先执行请求的人调用一次。如果你的执行路径是明确定义的,静态将对你有所帮助,但情况并非总是如此,尤其是在 GUI 应用程序等方面。staticstd::call_oncestd::call_one
6赞 Fantastic Mr Fox 4/3/2019 #2

想象一下,一个包含一些巨大数据的单例实例(出于某种原因):

class Singleton {
    public:  static Singleton& get();
    ...
    private: static std::unique_ptr<SingletonDataBase> instance;
}

我们如何确保 get 函数在正确调用时创建实例(无论出于何种原因,该实例非常大,不能进入静态内存空间)。我们如何实现这一目标?

  1. 使用 ?我猜有点丑。mutex
  2. 用?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

评论

0赞 The Quantum Physicist 4/3/2019
虽然这是有效的,但在日志记录的情况下,它可能不利于性能。您要为每个日志消息添加一个同步原子调用。在我的书中,日志记录应该尽可能低调。这就是异步日志记录很受欢迎的原因。我个人用来初始化记录器。与用于初始化标志的那个相同。static
0赞 Deduplicator 4/3/2019
为什么不直接使用标准的线程安全函数本地静态呢?简单得多。
1赞 Fantastic Mr Fox 4/3/2019
@TheQuantumPhysicist 粗体字说,诚然,这是为记录器示例而设计的。重点主要是想象一个非常大的单例。which for whatever reason is really large and can't go in static memory space
0赞 Fantastic Mr Fox 4/3/2019
@Deduplicator见上文。
0赞 Deduplicator 4/3/2019
嗯。通常,只有堆栈空间是有限的。但无论如何,可能至少有一个这样的怪人。
1赞 Caleth 4/3/2019 #3

什么时候应该使用?

当您想调用某物一次时。它简明扼要地说明了它在做什么。

替代方案

struct CallFooOnce { 
    CallFooOnce() { 
        foo(); 
    } 
}; 
static CallFooOnce foo_once;

有更多的样板,并引入了一个额外的名称,超过

static std::once_flag foo_once;
std::call_once(foo_once, foo);

评论

0赞 yuri kilochek 4/3/2019
foo_once也是一个附加名称。此外,您不太可能只有一个要调用的可调用对象。通常你想把它包装在一些错误处理代码中,你也可以把它放在承包商中。在这一点上,样板的数量没有太大区别。
0赞 Caleth 4/3/2019
@yurikilochek这两种选择都引入了名称 ,作为没有成员的对象。第一个还介绍了您可以将错误处理代码放在周围,就好像它在附近或更远的地方一样,然后重试foo_onceCallFooOncestd::call_once(foo_once, foo);foo()
0赞 Caleth 4/3/2019
@yurikilochek,如果投掷,这两种备选方案的行为相同。如果静态初始化是重入的,则静态初始化死锁,我找不到任何关于在这种情况下做什么的具体信息,但猜测它也是死锁foo()std::call_once
0赞 yuri kilochek 4/4/2019
关于类名的要点是你的,但如果该类也是本地的,这是一个小问题。通过错误处理,我的意思是这是一个 C 函数,它返回某种错误值并且不会抛出(否则,此初始化无论如何都应该位于支持 RAII 的适当类的构造函数中)。在这种情况下,您不能只传递给 ,而必须将其包装在一个函数中,该函数会检查错误值并可能将其转换为异常。这个包装函数也可以是一个构造函数。foofoostd::call_once
0赞 einpoklum 11/2/2020
这是一种人形的答案。当你想把事情做完的时候,你就打电话。问题是你什么时候需要调用一次?...这并不是为了拥有线程安全的单例,因为静态成员中的静态变量是线程安全的。do_thing()get()
8赞 Fabio 4/3/2019 #4

典型用途是当您希望在可能争用(多线程)的情况下按需初始化全局数据片段时。

假设你有结构

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

评论

0赞 Jerry Jeremiah 6/10/2020
如果a_singleton不需要全局,这难道行不通吗:是因为删除a_singleton无法完成吗?我想只有当 A 的析构函数不是微不足道时,这才重要......A *getA() { static A* a_singleton = new A(); return a_singleton; }
2赞 einpoklum 11/2/2020
@Fabio:我相信你错了;也就是说,局部静态确实是按需初始化的,但 C++11 标准要求此初始化是线程安全的。请参阅此帖子
6赞 Ben 6/1/2021 #5

缓存和延迟评估。假设一个不可变类具有存储成本低但计算成本高的属性 。与其按需计算或预先计算,不如这样做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 张幻灯片)。

评论

0赞 Ben 9/29/2021
如果使用此模式,如果我们想允许重置状态,遵循 herbsutter.com/2013/05/24/gotw-6a-const-correctness-part-1-3 方法并发安全性的指导,而不是非方法的并发安全性,我认为我们可以添加一个 foo()' 就像操作 .constconstprotected: 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
-1赞 Gabriel Ceron Viveros 6/13/2021 #6

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;
  } 

};

评论

0赞 Sneftel 6/16/2021
这些信息,包括单例的例子,已经在其他答案中给出了。
0赞 Alex Guteniev 11/17/2021 #7

其他已经提到的答案,效果可以通过魔术静电或使用来实现。call_oncemutex

我想专注于性能和极端情况处理。

使用互斥锁比在快速路径上慢(当目标函数已被调用时)。这是因为获取/释放将是原子 RMW,而查询只是读取。您可以通过实现双重检查锁定来修复性能,但这会使程序复杂化。call_oncemutexcall_oncemutex

如果首先尝试仅在程序退出时调用目标函数,则使用 call_once 也比互斥锁更安全。 不一定是可轻易破坏的,因此全局互斥锁可能会在调用目标时被销毁。mutex

Magic static 也可能在快速路径上快速运行,只需读取以查询对象是否已初始化。magic static call_once 的性能可能取决于实现,但通常 magic static 有更多的优化机会,因为它不必在变量中公开状态,并且必须由编译器实现,而不仅仅是在库中实现。

特定于 MSVC:magic statics 采用线程本地存储。这比 更优化。但是,可以通过 /Zc:threadSafeInit- 关闭神奇的静态支持。关闭它的能力是由于一些线程本地存储的缺点。 无法关闭。在 Visual Studio 2015 之前,没有很好地实现,并且根本没有实现魔术静态,因此如果没有第三方库,很难获得一次性初始化。call_oncecall_oncecall_once