在多线程编程中,经常会遇到这样的需求:某些函数或操作只能被调用一次,特别是在初始化某些全局或共享资源时。C++11 提供了一个简洁的解决方案:std::call_once()
,它能够保证在多线程环境中某个函数仅被执行一次,而不会引发竞争条件(Race Condition)。
一、为什么需要 std::call_once()
?
在多线程环境中,如果多个线程试图同时初始化某些共享资源,会引发不确定的行为。例如,两个线程可能同时执行初始化逻辑,导致资源被初始化多次,进而引发逻辑错误。为了避免这种情况,我们需要确保初始化逻辑只会被执行一次。传统的加锁机制(如 std::mutex
)虽然可以解决这个问题,但它的效率较低,且可能导致线程频繁阻塞。
为了解决这个问题,C++11 引入了 std::call_once()
,它结合了 std::once_flag
,能够高效地确保某个函数在多线程环境下只被调用一次。相比于传统的加锁机制,std::call_once()
更轻量,也更适合这种单次初始化的场景。
二、 std::call_once()
的用法
std::call_once()
是一个模板函数,其原型如下:
|
|
flag
是一个std::once_flag
对象,用于记录某个操作是否已经执行过。fn
是需要执行的函数或可调用对象(可以是普通函数、lambda、函数对象等)。args
是传递给fn
的参数。
当 std::call_once()
被调用时,它会检查 std::once_flag
的状态。如果 flag
的状态表明函数从未执行过,则 fn
会被执行,之后 flag
会被设置为 “已执行”。即使多个线程同时调用 std::call_once()
,也只有一个线程会真正执行 fn
。
三、示例:多线程中的 std::call_once()
下面的例子演示了如何使用 std::call_once()
来保证某个函数只被执行一次。
示例代码:
|
|
输出:
|
|
四、代码解析
-
once_flag
的使用: 在代码中,我们声明了一个全局变量onceflag
,它用于标记某个操作(即once_func()
)是否已经被执行过。这个对象是std::call_once()
保证函数只被执行一次的核心。 -
std::call_once()
: 在每个线程的执行函数func()
中,调用了std::call_once(onceflag, once_func, ...)
,其中onceflag
是用于标记的once_flag
对象,而once_func
是需要保证只执行一次的函数。即使有多个线程并发调用std::call_once()
,只有一个线程会真正执行once_func()
,其他线程会跳过该函数的执行。 -
线程并发执行: 两个线程
t1
和t2
被创建并执行,分别调用func()
函数。尽管两个线程都调用了std::call_once()
,但我们可以看到,once_func()
只被执行了一次。 -
确保资源的正确回收: 使用
t1.join()
和t2.join()
来等待线程完成并确保资源正确回收。
五、 std::call_once()
的应用场景
std::call_once()
最常见的应用场景是线程安全的单例模式(Singleton Pattern)。在单例模式中,我们希望某个对象只被初始化一次,无论有多少个线程试图访问它。例如:
|
|
在这个例子中,Singleton::getInstance()
使用 std::call_once()
来确保 Singleton
对象只被初始化一次,即使有多个线程同时调用 getInstance()
。
六、总结
std::call_once()
提供了一种高效、安全的方法来保证某个函数在多线程环境下只被执行一次。它结合 std::once_flag
,避免了传统锁机制带来的性能开销,简化了代码结构,同时大大提高了并发编程中的安全性。
无论是在实现单例模式、初始化全局资源,还是处理某些仅需执行一次的操作时,std::call_once()
都是一个非常值得推荐的工具。