C++多线程06:std::call_once

news/2024/11/9 4:43:00/

C++多线程:std::call_once

文章目录

  • C++多线程:std::call_once

  • 在多线程的环境下,有些时候我们不需要某个函数被调用多次或者某些变量被初始化多次,它们仅仅只需要被调用一次或者初始化一次即可。很多时候我们为了初始化某些数据会写出如下代码,这些代码在单线程中是没有任何问题的,但是在多线程中就会出现不可预知的问题。
bool initialized = false;
void foo() {if (!initialized) {do_initialize ();  //1initialized = true;}
}
复制代码
  • 为了解决上述多线程中出现的资源竞争导致的数据不一致问题,我们大多数的处理方法就是使用互斥锁来处理。只要上面①处进行保护,这样共享数据对于并发访问就是安全的。如下:
bool initialized = false;
std::mutex resource_mutex;void foo() {std::unique_lock<std::mutex> lk(resource_mutex);  // 所有线程在此序列化 if(!initialized) {do_initialize ();  // 只有初始化过程需要保护 }initialized = true;lk.unlock();// do other;
}
复制代码
  • 但是,为了确保数据源已经初始化,每个线程都必须等待互斥量。为此,还有人想到使用“双重检查锁模式”的办法来提高效率,如下:
bool initialized = false;
std::mutex resource_mutex;void foo() {if(!initialized) {  // 1std::unique_lock<std::mutex> lk(resource_mutex);  // 2 所有线程在此序列化 if(!initialized) {do_initialize ();  // 3 只有初始化过程需要保护 }initialized = true;}// do other;  // 4
}
复制代码
  • 第一次读取变量initialized时不需要获取锁①,并且只有在initialized为false时才需要获取锁。然后,当获取锁之后,会再检查一次initialized变量② (这就是双重检查的部分),避免另一线程在第一次检查后再做初始化,并且让当前线程获取锁。

  • 但是上面这种情况也存在一定的风险,具体可以查阅著名的《C++和双重检查锁定模式(DCLP)的风险》。

  • 对此,C++标准委员会也认为条件竞争的处理很重要,所以C++标准库提供了更好的处理方法:使用std::call_once函数来处理,其定义在头文件#include<mutex>中。std::call_once函数配合std::once_flag可以实现:多个线程同时调用某个函数,它可以保证多个线程对该函数只调用一次。它的定义如下:

struct once_flag
{constexpr once_flag() noexcept;once_flag(const once_flag&) = delete;once_flag& operator=(const once_flag&) = delete;
};template<class Callable, class ...Args>
void call_once(once_flag& flag, Callable&& func, Args&&... args);
复制代码
  • 他接受的第一个参数类型为std::once_flag,它只用默认构造函数构造,不能拷贝不能移动,表示函数的一种内在状态。后面两个参数很好理解,第一个传入的是一个Callable。Callable简单来说就是可调用的东西,大家熟悉的有函数、函数对象(重载了operator()的类)、std::function和函数指针,C++11新标准中还有std::bindlambda(可以查看我的上一篇文章)。最后一个参数就是你要传入的参数。 在使用的时候我们只需要定义一个non-local的std::once_flag(非函数局部作用域内的),在调用时传入参数即可,如下所示:
#include <iostream>
#include <thread>
#include <mutex>std::once_flag flag1;
void simple_do_once() {std::call_once(flag1, [](){ std::cout << "Simple example: called once\n"; });
}int main() {std::thread st1(simple_do_once);std::thread st2(simple_do_once);std::thread st3(simple_do_once);std::thread st4(simple_do_once);st1.join();st2.join();st3.join();st4.join();
}
复制代码
  • call_once保证函数func只被执行一次,如果有多个线程同时执行函数func调用,则只有一个活动线程(active call)会执行函数,其他的线程在这个线程执行返回之前会处于”passive execution”(被动执行状态)——不会直接返回,直到活动线程对func调用结束才返回。对于所有调用函数func的并发线程,数据可见性都是同步的(一致的)。

  • 但是,如果活动线程在执行func时抛出异常,则会从处于”passive execution”状态的线程中挑一个线程成为活动线程继续执行func,依此类推。一旦活动线程返回,所有”passive execution”状态的线程也返回,不会成为活动线程。(实际上once_flag相当于一个锁,使用它的线程都会在上面等待,只有一个线程允许执行。如果该线程抛出异常,那么从等待中的线程中选择一个,重复上面的流程)。

  • std::call_once在签名设计时也很好地考虑到了参数传递的开销问题,可以看到,不管是Callable还是Args,都使用了&&作为形参。他使用了一个template中的reference fold(我前面的文章也有介绍过),简单分析:

  1. 如果传入的是一个右值,那么Args将会被推断为Args
  2. 如果传入的是一个const左值,那么Args将会被推断为const Args&
  3. 如果传入的是一个non-const的左值,那么Args将会被推断为Args&
  • 也就是说,不管你传入的参数是什么,最终到达std::call_once内部时,都会是参数的引用(右值引用或者左值引用),所以说是零拷贝的。那么还有一步呢,我们还得把参数传到可调用对象里面执行我们要执行的函数,这一步同样做到了零拷贝,这里用到了另一个标准库的技术std::forward(我前面的文章也有介绍过)。

如下,如果在函数执行中抛出了异常,那么会有另一个在once_flag上等待的线程会执行。

#include <iostream>
#include <thread>
#include <mutex>std::once_flag flag;
inline void may_throw_function(bool do_throw) {// only one instance of this function can be run simultaneouslyif (do_throw) {std::cout << "throw\n"; // this message may be printed from 0 to 3 times// if function exits via exception, another function selectedthrow std::exception();}std::cout << "once\n"; // printed exactly once, it's guaranteed that// there are no messages after it
}inline void do_once(bool do_throw) {try {std::call_once(flag, may_throw_function, do_throw);} catch (...) {}
}int main() {std::thread t1(do_once, true);std::thread t2(do_once, true);std::thread t3(do_once, false);std::thread t4(do_once, true);t1.join();t2.join();t3.join();t4.join();
}
复制代码

std::call_once 也可以用在类中:

#include <iostream>
#include <mutex>
#include <thread>class A {public:void f() {std::call_once(flag_, &A::print, this);std::cout << 2;}private:void print() { std::cout << 1; }private:std::once_flag flag_;
};int main() {A a;std::thread t1{&A::f, &a};std::thread t2{&A::f, &a};t1.join();t2.join();
}  // 122
复制代码
  • 还有一种初始化过程中潜存着条件竞争:static 局部变量在声明后就完成了初始化,这存在潜在的 race condition,如果多线程的控制流同时到达 static 局部变量的声明处,即使变量已在一个线程中初始化,其他线程并不知晓,仍会对其尝试初始化。很多在不支持C++11标准的编译器上,在实践过程中,这样的条件竞争是确实存在的,为此,C++11 规定,如果 static 局部变量正在初始化,线程到达此处时,将等待其完成,从而避免了 race condition,只有一个全局实例时,对于C++11,可以直接用 static 而不需要 std::call_once,也就是说,在只需要一个全局实例情况下,可以成为std::call_once的替代方案,典型的就是单例模式了:
template <typename T>
class Singleton {public:static T& Instance();Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;private:Singleton() = default;~Singleton() = default;
};template <typename T>
T& Singleton<T>::Instance() {static T instance;return instance;
}
复制代码

今天的内容就到这里了。

参考:

std::call_once - C++中文 - API参考文档 (apiref.com)


http://www.ppmy.cn/news/18422.html

相关文章

【算法基础】归并排序

目录 一、归并排序的思想 二、归并排序的步骤 三、归并的方式 四、代码模板 一、归并排序的思想 归并排序和快速排序一样&#xff0c;都是分治的思想。它是将一个无序的数组一分为二&#xff0c;最后再合二为一&#xff0c;将两个有序数组合并成为一个有序数组。 时间复杂…

2023年新年烟花代码(背景音乐完整版)

文章目录前言烟花效果展示使用教程查看源码HTML代码CSS代码JavaScript新年祝福前言 大家过年好&#xff01;新春佳节&#xff0c;在这个充满喜悦的日子里&#xff0c;愿新年的钟声带给你一份希望和期待&#xff0c;我相信&#xff0c;时空的距离不能阻隔你我&#xff0c;我的祝…

第五层:C++中的运算符重载

文章目录前情回顾运算符重载概念为什么会出现运算符重载运算符重载中函数名格式加减运算符重载作用实现左移运算符重载作用左移运算符是什么&#xff1f;实现递增递减运算符作用实现前置后置赋值运算符重载关系运算符重载作用实现函数调用运算符重载第二种重载掌握&#xff01;…

Zookeeper 【下载与安装,基本使用】

目录 1. 什么是zookeeper 2. zookeeper下载与安装 3. Zookeeper 测试 1. 什么是zookeeper zookeeper实际上是yahoo开发的&#xff0c;用于分布式中一致性处理的框架。最初其作为研发Hadoop时的副产品。 由于分布式系统中一致性处理较为困难&#xff0c;其他的分布式系统没有…

ACM模板(数学算法)

目录 〇&#xff0c;全文说明、宏定义代码 一&#xff0c;单例、快速幂、数论 二&#xff0c;并查集、DancingLink、图论 三&#xff0c;test 〇&#xff0c;全文说明、宏定义代码 所有接口分三类&#xff1a; Sieve类继承了单例模板&#xff0c;用单例对象去调用接口。并…

【阅读笔记】《重构》 第五六章

第五章 重构列表 重构的记录格式 建造一个重构词汇表一个简短概要&#xff0c;解决的问题&#xff0c;应该做的事&#xff0c;展示重构前后示例动机&#xff0c;为什么要重构做法&#xff0c;介绍如何进行此一重构范例&#xff0c;说明重构手法如何运作 寻找引用点 利用文本…

vs code中的platformIO插件,完成Arduino的程序编写,导入,安装开发板管理库

准备工作 vs code已经安装好&#xff0c;扩展插件plateformIO也安装好。&#xff08;下图是platformIO安装方式&#xff09; platformIO界面功能介绍和简单使用 新建Arduino项目 选择正确的开发板型号&#xff0c;和自己习惯的编译框架。打开后有一个.ini的配置文件&#x…

MySQL详细教程,2023年硬核学习路线

文章目录前言1. 数据库的相关概念1.1 数据1.2 数据库1.3 数据库管理系统1.4 数据库系统1.5 SQL2. MySQL数据库2.1 MySQL安装2.2 MySQL配置2.2.1 添加环境变量2.2.2 新建配置文件2.2.3 初始化MySQL2.2.4 注册MySQL服务2.2.5 启动MySQL服务2.3 MySQL登录和退出2.4 MySQL卸载2.5 M…