在多线程编程中,经常会遇到这样的需求:某些函数或操作只能被调用一次,特别是在初始化某些全局或共享资源时。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() 是一个模板函数,其原型如下:

1
2
template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& fn, Args&&... args );
  • 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() 来保证某个函数只被执行一次。

示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <thread>        // 包含 std::thread。
#include <mutex>         // 包含 std::once_flag 和 std::call_once。
#include <chrono>        // 包含 std::chrono::seconds。

using namespace std;

// 定义 once_flag 对象,用于标记某个操作是否已经执行。
once_flag onceflag;

// 打算只被执行一次的函数。
void once_func(const int bh, const string& str)  { 
    cout << "once_func() bh = " << bh << ", str = " << str << endl;
}

// 模拟多个线程执行的函数。
void func(int bh, const string& str) {
    // 确保 once_func 只被执行一次。
    call_once(onceflag, once_func, 0, "各位观众,我要开始表白了。");

    // 模拟多次表白的操作。
    for (int ii = 1; ii <= 3; ii++) {
        cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
        this_thread::sleep_for(chrono::seconds(1));    // 休眠1秒。
    }
}

int main() {
    // 创建两个线程,模拟多个线程并发执行。
    thread t1(func, 3, "我是一只傻傻鸟。");
    thread t2(func, 8, "我有一只小小鸟。");

    // 等待线程结束。
    t1.join();
    t2.join();
}

输出:

1
2
3
4
5
6
7
once_func() bh = 0, str = 各位观众,我要开始表白了。
第1次表白:亲爱的3号,我是一只傻傻鸟。
第1次表白:亲爱的8号,我有一只小小鸟。
第2次表白:亲爱的3号,我是一只傻傻鸟。
第2次表白:亲爱的8号,我有一只小小鸟。
第3次表白:亲爱的3号,我是一只傻傻鸟。
第3次表白:亲爱的8号,我有一只小小鸟。

四、代码解析

  1. once_flag 的使用: 在代码中,我们声明了一个全局变量 onceflag,它用于标记某个操作(即 once_func())是否已经被执行过。这个对象是 std::call_once() 保证函数只被执行一次的核心。

  2. std::call_once(): 在每个线程的执行函数 func() 中,调用了 std::call_once(onceflag, once_func, ...),其中 onceflag 是用于标记的 once_flag 对象,而 once_func 是需要保证只执行一次的函数。即使有多个线程并发调用 std::call_once(),只有一个线程会真正执行 once_func(),其他线程会跳过该函数的执行。

  3. 线程并发执行: 两个线程 t1t2 被创建并执行,分别调用 func() 函数。尽管两个线程都调用了 std::call_once(),但我们可以看到,once_func() 只被执行了一次。

  4. 确保资源的正确回收: 使用 t1.join()t2.join() 来等待线程完成并确保资源正确回收。


五、 std::call_once() 的应用场景

std::call_once() 最常见的应用场景是线程安全的单例模式(Singleton Pattern)。在单例模式中,我们希望某个对象只被初始化一次,无论有多少个线程试图访问它。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Singleton {
private:
    static Singleton* instance;
    static once_flag flag;

    Singleton() {
        cout << "Singleton 构造函数执行" << endl;
    }

public:
    static Singleton* getInstance() {
        call_once(flag, []() {
            instance = new Singleton();
        });
        return instance;
    }
};

Singleton* Singleton::instance = nullptr;
once_flag Singleton::flag;

在这个例子中,Singleton::getInstance() 使用 std::call_once() 来确保 Singleton 对象只被初始化一次,即使有多个线程同时调用 getInstance()


六、总结

std::call_once() 提供了一种高效、安全的方法来保证某个函数在多线程环境下只被执行一次。它结合 std::once_flag,避免了传统锁机制带来的性能开销,简化了代码结构,同时大大提高了并发编程中的安全性。

无论是在实现单例模式、初始化全局资源,还是处理某些仅需执行一次的操作时,std::call_once() 都是一个非常值得推荐的工具。