只执行一次,不多不少!C++11的线程安全神器std::once_flag与call_once详解

大家好啊,我是小康。

今天咱们聊一个看起来很小但实际超级实用的话题 —— C++11中的std::once_flagcall_once。听起来很高大上?别担心,我保证用最接地气的方式给你讲明白,绝不玩文绉绉的!

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

一个生活中的小故事

想象一下这个场景:你和三个朋友一起住,早上大家都要用洗衣机。如果每个人都去按一次洗衣机的"开始"按钮会发生什么?对,洗衣机会被重启好几次,衣服永远洗不完!

理想情况是:无论谁先到洗衣机前,只需要有一个人按一次开始按钮就够了。其他人看到洗衣机已经在运转,就不需要再按了。

这就是我们今天要讲的std::once_flagcall_once要解决的问题!

为什么需要"只执行一次"的机制?

在多线程程序中,有些初始化工作我们希望:

  • 必须执行(不能少)
  • 只能执行一次(不能多)
  • 所有线程都要等这个初始化完成后才能继续

最典型的例子就是单例模式(Singleton Pattern):无论多少个线程同时请求,系统中某个对象只能有一个实例。

没有call_once之前,大家都这么写:

// 传统写法,容易出问题
class Singleton {
private:
    static Singleton* instance;
    static std::mutex mutex;
    
    Singleton() { /* 构造函数 */ }
    
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {  // 第一次检查
            std::lock_guard<std::mutex> lock(mutex);
            if (instance == nullptr) {  // 第二次检查
                instance = new Singleton();
            }
        }
        return instance;
    }
};

看看这个"双重检查锁定"模式,又臭又长,还容易写错。更糟的是,在某些内存模型下,这段代码仍然可能有问题!

救星来了:std::once_flag 和 call_once

C++11引入了两个救星:

  • std::once_flag:一面"只执行一次"的旗帜
  • std::call_once:确保某个函数在多线程环境下只执行一次的工具

让我们看看它们是怎么用的:

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

std::once_flag flag;  // 创建一个"只执行一次"的旗帜

void do_once() {
    std::cout << "这段代码只会执行一次!" << std::endl;
}

void thread_func() {
    // 无论多少线程调用这个函数,do_once()只会执行一次
    std::call_once(flag, do_once);
    std::cout << "线程 " << std::this_thread::get_id() << " 执行完毕" << 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;
}

运行这段代码,你会发现"这段代码只会执行一次!"这句话确实只打印了一次,而"线程xx执行完毕"会打印三次。

工作原理:它是怎么做到的?

std::once_flagcall_once背后的工作原理其实很简单:

  1. once_flag是一个标记,记录关联的函数是否已经执行过
  2. 当第一个线程调用call_once时,它会执行指定的函数
  3. 其他线程调用同一个call_once(使用同一个flag)时,会等待第一个线程执行完毕
  4. 一旦函数执行完成,所有线程都会继续运行,但函数不会被重复执行

单例模式的最佳实践

现在,让我们用std::call_once来改写单例模式:

class Singleton {
private:
    static std::once_flag init_flag;
    static Singleton* instance;
    
    Singleton() { 
        std::cout << "创建单例对象!" << std::endl;
    }
    
public:
    static Singleton* getInstance() {
        std::call_once(init_flag, []() {
            instance = new Singleton();
        });
        return instance;
    }
    
    void doSomething() {
        std::cout << "单例正在工作..." << std::endl;
    }
};

// 在类外初始化静态成员
std::once_flag Singleton::init_flag;
Singleton* Singleton::instance = nullptr;

是不是简洁了很多?没有复杂的双重检查,没有手动管理互斥锁,代码量减少了一半!

更多实际应用场景

除了单例模式,std::call_once还适用于很多场景:

1. 延迟初始化昂贵资源

class DatabaseConnection {
private:
    std::once_flag conn_init;
    Connection* conn;
    
public:
    Connection* getConnection() {
        std::call_once(conn_init, [this]() {
            std::cout << "建立数据库连接(耗时操作)..." << std::endl;
            conn = new Connection("db://example");
        });
        return conn;
    }
};

2. 线程池的一次性初始化

class ThreadPool {
private:
    std::once_flag init_flag;
    std::vector<std::thread> threads;
    
public:
    void initialize(size_t thread_count) {
        std::call_once(init_flag, [this, thread_count]() {
            for (size_t i = 0; i < thread_count; ++i) {
                threads.emplace_back(&ThreadPool::worker_thread, this);
            }
        });
    }
    
    void worker_thread() {
        // 线程工作内容...
    }
};

3. 配置文件的一次性加载

class Configuration {
private:
    std::once_flag load_flag;
    std::map<std::string, std::string> settings;
    
public:
    std::string getSetting(const std::string& key) {
        std::call_once(load_flag, [this]() {
            std::cout << "加载配置文件..." << std::endl;
            // 加载配置到settings映射表...
        });
        return settings[key];
    }
};

比较:call_once vs 其他线程安全方式

那么,std::call_once比其他方法好在哪里呢?

  1. 对比静态局部变量:C++11保证静态局部变量的初始化是线程安全的,但call_once更灵活,可以在任何地方调用,不限于函数作用域
  2. 对比互斥锁:比手动管理互斥锁更简洁,不容易出错
  3. 对比原子操作:比使用std::atomic编写复杂的初始化逻辑更清晰
  4. 性能优势:在多数实现中,call_once采用了类似读写锁的优化,多线程并发时性能更好

容易踩的坑

使用std::call_once虽然简单,但还是有一些坑需要注意:

  1. 一个flag只能用于一次初始化:如果你需要多个不同的一次性初始化,就需要多个once_flag
  2. 异常处理:如果被调用的函数抛出异常,call_once会认为初始化失败,下次还会尝试执行
  3. 避免死锁:不要在call_once调用的函数中再次使用同一个once_flag,否则会死锁
  4. 线程退出once_flag不会在线程退出时自动重置,它是全局状态

来看一个异常处理的例子:

std::once_flag flag;

void may_throw() {
    std::call_once(flag, []() {
        std::cout << "尝试初始化..." << std::endl;
        throw std::runtime_error("初始化失败!");
    });
}

int main() {
    try {
        may_throw();  // 第一次尝试初始化,会抛出异常
    } catch (const std::exception& e) {
        std::cout << "捕获异常: " << e.what() << std::endl;
    }
    
    try {
        may_throw();  // 由于上次失败,会再次尝试初始化
    } catch (const std::exception& e) {
        std::cout << "再次捕获异常: " << e.what() << std::endl;
    }
    
    return 0;
}

输出结果会显示初始化被尝试了两次!

总结:什么时候用call_once?

std::once_flagcall_once最适合以下场景:

  1. 需要线程安全的一次性初始化
  2. 单例模式的实现
  3. 共享资源的延迟初始化
  4. 需要确保某个操作在多线程环境下只执行一次

记住,它是C++11标准库给我们提供的"只执行一次"的保证,远比我们自己实现双重检查锁定更可靠、更简单。

你们有没有在项目中用过这个功能?欢迎在评论区分享你的经验!

写在最后

感觉对线程安全有点"开窍"了吗?如果这篇文章帮你解决了困惑,不妨动动手指支持一下!👇

❤️ 一键三连点赞、收藏、关注

🔒 关注我的公众号「跟着小康学编程」,这里没有枯燥说教,只有接地气的技术分享!

在我的代码小宇宙里,我们一起:

  • 🕵️‍♂️ 揭开那些面试官最爱问的 C++ 多线程陷阱,让你从"面试炮灰"变"offer收割机"
  • 🔧 拆解并发编程的各种"黑魔法",让多线程问题不再让你头大
  • 🚀 分享那些我踩过的坑(有些坑深得能把人埋了…)让你少走弯路
  • 💡 传授性能调优秘笈,让你的代码跑得比同事快3倍(然后假装很轻松)
  • 🐧 深入Linux底层机制,做个内核态、用户态自由切换的开发者

公众号每周更新!不定期分享各种业内趣事和编程彩蛋,绝对是通勤路上最解压的技术号!

记住,在编程的世界里——没有解决不了的Bug,只有不够巧妙的思路。我们下期见!

💬 对了,如果你在工作中遇到过类似的并发问题,或者有其他想了解的C++话题,欢迎在评论区留言

怎么关注我的公众号?

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

另外,小康还建了一个技术交流群,专门聊技术、答疑解惑。如果你在读文章时碰到不懂的地方,随时欢迎来群里提问!我会尽力帮大家解答,群里还有不少技术大佬在线支援,咱们一起学习进步,互相成长!

想找我?加我微信即可,微信号:jkfwdkf ,备注 「加群

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值