大家好啊,我是小康。
今天咱们聊一个看起来很小但实际超级实用的话题 —— C++11中的std::once_flag和call_once。听起来很高大上?别担心,我保证用最接地气的方式给你讲明白,绝不玩文绉绉的!
微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆
一个生活中的小故事
想象一下这个场景:你和三个朋友一起住,早上大家都要用洗衣机。如果每个人都去按一次洗衣机的"开始"按钮会发生什么?对,洗衣机会被重启好几次,衣服永远洗不完!
理想情况是:无论谁先到洗衣机前,只需要有一个人按一次开始按钮就够了。其他人看到洗衣机已经在运转,就不需要再按了。
这就是我们今天要讲的std::once_flag和call_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_flag和call_once背后的工作原理其实很简单:
once_flag是一个标记,记录关联的函数是否已经执行过- 当第一个线程调用
call_once时,它会执行指定的函数 - 其他线程调用同一个
call_once(使用同一个flag)时,会等待第一个线程执行完毕 - 一旦函数执行完成,所有线程都会继续运行,但函数不会被重复执行
单例模式的最佳实践
现在,让我们用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比其他方法好在哪里呢?
- 对比静态局部变量:C++11保证静态局部变量的初始化是线程安全的,但
call_once更灵活,可以在任何地方调用,不限于函数作用域 - 对比互斥锁:比手动管理互斥锁更简洁,不容易出错
- 对比原子操作:比使用
std::atomic编写复杂的初始化逻辑更清晰 - 性能优势:在多数实现中,
call_once采用了类似读写锁的优化,多线程并发时性能更好
容易踩的坑
使用std::call_once虽然简单,但还是有一些坑需要注意:
- 一个flag只能用于一次初始化:如果你需要多个不同的一次性初始化,就需要多个
once_flag - 异常处理:如果被调用的函数抛出异常,
call_once会认为初始化失败,下次还会尝试执行 - 避免死锁:不要在
call_once调用的函数中再次使用同一个once_flag,否则会死锁 - 线程退出:
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_flag和call_once最适合以下场景:
- 需要线程安全的一次性初始化
- 单例模式的实现
- 共享资源的延迟初始化
- 需要确保某个操作在多线程环境下只执行一次
记住,它是C++11标准库给我们提供的"只执行一次"的保证,远比我们自己实现双重检查锁定更可靠、更简单。
你们有没有在项目中用过这个功能?欢迎在评论区分享你的经验!
写在最后
感觉对线程安全有点"开窍"了吗?如果这篇文章帮你解决了困惑,不妨动动手指支持一下!👇
❤️ 一键三连:点赞、收藏、关注。
🔒 关注我的公众号「跟着小康学编程」,这里没有枯燥说教,只有接地气的技术分享!
在我的代码小宇宙里,我们一起:
- 🕵️♂️ 揭开那些面试官最爱问的 C++ 多线程陷阱,让你从"面试炮灰"变"offer收割机"
- 🔧 拆解并发编程的各种"黑魔法",让多线程问题不再让你头大
- 🚀 分享那些我踩过的坑(有些坑深得能把人埋了…)让你少走弯路
- 💡 传授性能调优秘笈,让你的代码跑得比同事快3倍(然后假装很轻松)
- 🐧 深入Linux底层机制,做个内核态、用户态自由切换的开发者
公众号每周更新!不定期分享各种业内趣事和编程彩蛋,绝对是通勤路上最解压的技术号!
记住,在编程的世界里——没有解决不了的Bug,只有不够巧妙的思路。我们下期见!
💬 对了,如果你在工作中遇到过类似的并发问题,或者有其他想了解的C++话题,欢迎在评论区留言!
怎么关注我的公众号?
微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆
另外,小康还建了一个技术交流群,专门聊技术、答疑解惑。如果你在读文章时碰到不懂的地方,随时欢迎来群里提问!我会尽力帮大家解答,群里还有不少技术大佬在线支援,咱们一起学习进步,互相成长!
想找我?加我微信即可,微信号:jkfwdkf ,备注 「加群」



408

被折叠的 条评论
为什么被折叠?



