c++的std::call_once和std::once_flag有什么用?

来源:菜鸟站长作者:俊华头衔:草根站长
导读:本期聚焦于小伙伴创作的《c++的std::call_once和std::once_flag有什么用?》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《c++的std::call_once和std::once_flag有什么用?》有用,将其分享出去将是对创作者最好的鼓励。

在多线程编程中,我们经常会遇到需要初始化全局资源或者单例对象的场景,如果多个线程同时检测到资源未初始化并同时执行初始化逻辑,就会出现重复初始化的问题,轻则浪费系统资源,重则导致程序逻辑错误。c++11标准引入的std::call_oncestd::once_flag就是专门用来解决这类线程安全初始化问题的工具,它们可以保证指定的初始化操作在多线程环境下只会被执行一次。

c++的std::call_once和std::once_flag有什么用?

std::once_flag和std::call_once的基本作用

std::once_flag是一个辅助类,用来标记某个初始化操作是否已经执行过,它本身不存储任何初始化逻辑,仅作为状态标识使用。而std::call_once是一个函数模板,接收std::once_flag对象和需要执行的初始化可调用对象,保证该可调用对象只会被执行一次。

这两个组件的核心价值在于:不需要开发者手动编写加锁、判断状态、解锁的复杂逻辑,就可以实现线程安全的单次初始化,避免了手动实现时可能出现的死锁、漏判状态等问题。

基本使用方法

使用std::call_oncestd::once_flag的流程非常简单,只需要三步:

  • 定义一个std::once_flag类型的全局或静态变量,作为初始化操作的状态标识
  • 编写需要执行的初始化逻辑,可以是函数、lambda表达式、函数对象等可调用对象
  • 在需要初始化的地方调用std::call_once,传入std::once_flag对象和初始化可调用对象

基础示例:线程安全的全局变量初始化

下面是一个简单的示例,模拟多个线程同时尝试初始化一个全局配置对象,保证配置只被初始化一次:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>  // 包含std::call_once和std::once_flag的头文件

// 全局配置结构体
struct GlobalConfig {
    int max_connection;
    std::string log_path;
};

// 全局配置指针,初始为空
GlobalConfig* g_config = nullptr;
// 初始化状态标识
std::once_flag g_config_init_flag;

// 初始化配置的函数
void init_global_config() {
    std::cout << "执行全局配置初始化" << std::endl;
    g_config = new GlobalConfig();
    g_config->max_connection = 100;
    g_config->log_path = "/var/log/app.log";
}

// 线程执行的函数,尝试获取配置
void thread_get_config() {
    // 调用call_once,保证init_global_config只执行一次
    std::call_once(g_config_init_flag, init_global_config);
    // 后续使用配置
    std::cout << "线程获取配置,最大连接数:" << g_config->max_connection << std::endl;
}

int main() {
    // 创建5个线程同时尝试获取配置
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(thread_get_config);
    }
    // 等待所有线程执行完成
    for (auto& t : threads) {
        t.join();
    }
    // 释放配置内存
    delete g_config;
    return 0;
}

运行上述代码,你会发现"执行全局配置初始化"这句话只会输出一次,即使有5个线程同时调用了std::call_once,初始化逻辑也只会被执行一次,后续线程会直接跳过初始化步骤,使用已经初始化好的配置对象。

使用lambda表达式作为初始化逻辑

如果初始化逻辑比较简单,也可以直接使用lambda表达式作为std::call_once的可调用对象,不需要单独定义初始化函数:

#include <iostream>
#include <thread>
#include <mutex>

class Singleton {
public:
    static Singleton* get_instance() {
        static std::once_flag init_flag;
        std::call_once(init_flag, [&]() {
            std::cout << "初始化单例对象" << std::endl;
            instance_ = new Singleton();
        });
        return instance_;
    }

private:
    Singleton() = default;
    static Singleton* instance_;
};

Singleton* Singleton::instance_ = nullptr;

void thread_func() {
    Singleton* obj = Singleton::get_instance();
    std::cout << "线程获取单例地址:" << obj << std::endl;
}

int main() {
    std::thread t1(thread_func);
    std::thread t2(thread_func);
    std::thread t3(thread_func);
    t1.join();
    t2.join();
    t3.join();
    return 0;
}

这个示例实现了一个简单的线程安全单例模式,通过std::call_once保证单例对象的初始化只执行一次,避免了传统双重检查锁实现单例时可能出现的指令重排问题。

使用注意事项

在使用std::call_oncestd::once_flag时,需要注意以下几点:

  • std::once_flag对象不能被拷贝或者移动,也不能被销毁后再次使用,通常定义为全局变量、静态变量或者长期存在的对象成员
  • 如果初始化可调用对象抛出了异常,那么std::call_once会认为初始化没有完成,后续其他线程调用时还会再次尝试执行初始化逻辑,直到初始化成功为止
  • std::call_once的初始化逻辑中如果包含加锁操作,需要注意避免死锁,比如不要在初始化逻辑中等待其他也在调用std::call_once的线程
  • std::once_flag不需要手动重置状态,它的状态一旦标记为已初始化,就会一直保持,无法回到未初始化状态

和其他初始化方式的对比

在c++11之前,开发者通常会用互斥锁加判断的方式实现线程安全初始化,比如下面的传统实现方式:

#include <mutex>

std::mutex g_init_mutex;
bool g_inited = false;
GlobalConfig* g_config = nullptr;

void init_config_old() {
    std::lock_guard<std::mutex> lock(g_init_mutex);
    if (!g_inited) {
        g_config = new GlobalConfig();
        g_inited = true;
    }
}

这种实现方式存在两个问题:一是每次调用都需要加锁判断,即使初始化已经完成,还是会有锁的开销;二是如果开发者忘记加锁或者判断逻辑写错,就会出现线程安全问题。而std::call_once的实现通常会利用系统底层的单次初始化机制,初始化完成后不会有额外的性能开销,同时也不需要开发者手动处理状态判断,更加安全可靠。

另外c++11之后还支持静态局部变量的线程安全初始化,即静态局部变量的初始化在多线程环境下只会执行一次,不过这种方式只适用于静态局部变量的场景,而std::call_once可以用于任意作用域的初始化逻辑,使用场景更加灵活。

适用场景总结

std::call_oncestd::once_flag主要适用于以下场景:

  • 全局资源、配置对象的线程安全初始化
  • 单例模式的对象初始化
  • 只需要执行一次的初始化逻辑,比如日志系统初始化、网络库初始化等
  • 需要在多个线程中触发,但只能执行一次的任意可调用逻辑

总的来说,这两个组件是c++多线程编程中非常实用的工具,能够帮助开发者用极低的成本实现线程安全的单次初始化,减少手动编写线程同步逻辑带来的错误风险。

std::call_oncestd::once_flag线程安全初始化C++修改时间:2026-07-06 10:03:37

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。