目录
1.引言
2.std::once_flag
3.std::call_once
4.使用场景
5.总结
1.引言
在多线程编程中,确保某些操作只执行一次是一个常见需求。std::call_once
是 C++11 中引入的一个模版函数,它能帮助我们在多线程环境下实现“单次调用”(once-only)的功能,避免重复执行同一操作。
常用的场景如Init()操作或一些系统参数的获取等。相对来说,std::call_once用法比较简单,配合std::once_flag即可实现。
2.std::once_flag
std::once_flag
是一个简单的标志类,用于指示某个初始化操作是否已经完成。它通常与 std::call_once
一起使用。std::once_flag
的实例在创建时处于未设置状态,表示相关的初始化操作尚未执行。一旦通过 std::call_once
成功执行了初始化操作,std::once_flag
的状态就被设置为已设置,表示初始化已经完成。
它的特征有:
- 一次性:
std::once_flag
的状态在创建时是未设置的(即表示初始化操作尚未执行)。一旦通过std::call_once
成功执行了与std::once_flag
关联的操作,std::once_flag
的状态就被设置为已设置,并且之后即使再次调用std::call_once
,也不会再次执行该操作。 - 线程安全:
std::once_flag
的设计保证了它是线程安全的,可以在多个线程之间安全地共享和修改。 - 不可复制:
std::once_flag
对象是不可复制的,这意味着你不能创建一个std::once_flag
的副本。这是为了确保状态的一致性,防止由于复制操作而导致的状态混乱。 - 不可移动:同样地,
std::once_flag
对象也是不可移动的,即你不能使用移动语义来转移std::once_flag
的所有权。
3.std::call_once
std::call_once
的设计目的是为了保证某个操作(如初始化)在多线程程序中只被调用一次。它结合了std::mutex
的功能,确保在程序的不同线程中,只有第一个调用该操作的线程会执行操作,其他线程会被阻塞,直到初始化操作完成,然后继续执行,但不会再次调用可调用对象。
在多线程程序中,通常有多种方法来同步线程的执行。std::call_once
使用了一个称为“标志”(std::once_flag
)的机制来标记某个操作是否已经执行过。只有当标志没有被设置时,操作才会被执行。其他线程会等待该操作完成,确保只执行一次。
std::call_once
接受两个参数:
-
once_flag:一个
std::once_flag
类型的对象,用于标记该操作是否已经执行过。 -
Function:一个可调用对象(如函数、函数指针、lambda 表达式等),这是要执行的操作。
基本用法:
#include <iostream>
#include <mutex>
#include <thread>static std::once_flag flag;void init() {std::cout << "Initialization function called once." << std::endl;
}void worker(int id) {std::call_once(flag, init);std::cout << "Worker thread " << id << " is running." << std::endl;
}int main() {std::thread t1(worker, 1);std::thread t2(worker, 2);std::thread t3(worker, 3);t1.join();t2.join();t3.join();return 0;
}
输出:
Initialization function called once.
Worker thread 1 is running.
Worker thread 2 is running.
Worker thread 3 is running.
在这个例子中,尽管有三个线程调用std::call_once
,但是init
只有在第一次调用时执行一次。其他线程等待操作完成后就会跳过该操作。
4.使用场景
1)单例模式的实现:
在单例模式中,类的实例只被创建一次,并且这个实例通过一个静态指针或引用被全局访问。std::call_once
可以确保即使多个线程同时尝试创建实例,也只会有一个线程成功执行创建操作。
2)延迟初始化:
有些资源或对象可能非常昂贵或耗时来创建,但并非在程序启动时就需要。使用 std::call_once
,可以在第一次真正需要使用这些资源时才进行初始化,从而节省时间和资源。
3)静态局部变量的线程安全初始化:
在 C++ 中,静态局部变量的初始化是线程不安全的(直到 C++11)。使用 std::call_once
可以确保静态局部变量的初始化在多线程环境中也是安全的。
4)配置或数据的加载:
如果程序需要从文件、数据库或其他外部源加载配置或数据,并且这些数据在程序运行期间不会改变,那么可以使用 std::call_once
来确保这些数据只被加载一次。
5)线程安全的日志记录初始化:
如果程序使用线程安全的日志记录库,并且这个库需要在第一次记录日志之前进行一些初始化工作,那么可以使用 std::call_once
来确保这些初始化工作只被执行一次。
6)插件或模块的加载:
在动态加载插件或模块的应用程序中,可以使用 std::call_once
来确保每个插件或模块只被加载和初始化一次。
示例代码:
以下是一个简单的示例,展示了如何使用 std::call_once
和 std::once_flag
来实现单例模式:
#include <iostream>
#include <mutex>
#include <thread>class Singleton {
public:static Singleton& getInstance() {std::call_once(instanceFlag, []() { instance = new Singleton(); });return *instance;}void doSomething() {std::cout << "Singleton instance is doing something." << std::endl;}private:Singleton() { std::cout << "Singleton instance created." << std::endl; }~Singleton() { delete this; } // 注意:在实际应用中,通常使用智能指针来管理单例的生命周期。static Singleton* instance;static std::once_flag instanceFlag;
};Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::instanceFlag;void worker(int id) {Singleton& s = Singleton::getInstance();s.doSomething();
}int main() {std::thread t1(worker, 1);std::thread t2(worker, 2);t1.join();t2.join();return 0;
}
在这个例子中,Singleton
类有一个私有构造函数和一个静态的 getInstance
方法。getInstance
方法使用 std::call_once
来确保 instance
只被创建一次。即使 worker
函数被多个线程同时调用,Singleton
的实例也只会被创建一次。
5.总结
std::call_once
是 C++11 提供的模版函数,能够帮助我们在多线程环境中安全、简洁地实现“单次调用”的逻辑。通过合理使用std::call_once
,我们可以确保线程安全地执行初始化和其他需要保证只执行一次的操作,从而提高程序的稳定性和效率。正确的使用场景、最佳实践以及性能优化技巧将进一步增强我们在多线程编程中的能力。
推荐阅读:
设计模式之单例模式-CSDN博客