C++多线程std::call_once的使用

在多线程的环境下,有些时候我们不需要某个函数被调用多次或者某些变量被初始化多次,它们仅仅只需要被调用一次或者初始化一次即可。很多时候我们为了初始化某些数据会写出如下代码,这些代码在单线程中是没有任何问题的,但是在多线程中就会出现不可预知的问题。

bool initialized = false;
void foo() {
    if (!initialized) {
        do_initialize ();  //1
        initialized = 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) {  // 1
        std::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(我前面的文章也有介绍过),简单分析:

  • 如果传入的是一个右值,那么Args将会被推断为Args
  • 如果传入的是一个const左值,那么Args将会被推断为const Args&
  • 如果传入的是一个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 simultaneously
    if (do_throw) {
        std::cout << "throw\n"; // this message may be printed from 0 to 3 times
        // if function exits via exception, another function selected
        throw 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)

到此这篇关于C++多线程std::call_once的使用的文章就介绍到这了,更多相关C++ std::call_once内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++11中std::declval的实现机制浅析

    本文主要给大家介绍了关于C++11中std::declval实现机制的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: 在vs2013中,declval定义如下 template <_Ty> typenamea dd_rvalue_reference<_Ty>::type declval() _noexcept; 其中,add_rvalue_reference为一个traits,定义为 template <_Ty> struct add_rvalue_ref

  • C++11中lambda、std::function和std:bind详解

    前言 在C++11新标准中,语言本身和标准库都增加了很多新内容,本文只涉及了一些皮毛.不过我相信这些新特性当中有一些,应该成为所有C++开发者的常规装备.本文主要介绍了C++11中lambda.std::function和std:bind,下面来一起看看详细的介绍吧. lambda 表达式 C++11中新增了lambda 表达式这一语言特性.lambda表达式可以让我们快速和便捷的创建一个"函数". 下面是lambda表达式的语法: [ capture-list ] { body }

  • C++11 并发指南之std::thread 详解

    上一篇博客<C++11 并发指南一(C++11 多线程初探)>中只是提到了 std::thread 的基本用法,并给出了一个最简单的例子,本文将稍微详细地介绍 std::thread 的用法. std::thread 在 <thread> 头文件中声明,因此使用 std::thread 时需要包含 <thread> 头文件. std::thread 构造 default (1) thread() noexcept; initialization (2) template

  • C++11中std::future的具体使用方法

    C++11中的std::future是一个模板类.std::future提供了一种用于访问异步操作结果的机制.std::future所引用的共享状态不能与任何其它异步返回的对象共享(与std::shared_future相反)( std::future references shared state that is not shared with any other asynchronous return objects (as opposed to std::shared_future)).一

  • C/C++中关于std::string的compare陷阱示例详解

    前言 C++ 语言是个十分优秀的语言,但优秀并不表示完美.还是有许多人不愿意使用C或者C++,为什么?原因众多,其中之一就是C/C++的文本处理功能太麻烦,用起来很不方便.以前没有接触过其他语言时,每当别人这么说,我总是不屑一顾,认为他们根本就没有领会C++的精华,或者不太懂C++,现在我接触 perl, php, 和Shell脚本以后,开始理解了以前为什么有人说C++文本处理不方便了. 总之,有了string 后,C++的字符文本处理功能总算得到了一定补充,加上配合STL其他容器使用,其在文本

  • C++ 11 std::function和std::bind使用详解

    cocos new 出新的项目之后,仔细阅读代码,才发现了一句3.0区别于2.0的代码: auto closeItem = MenuItemImage::create( "CloseNormal.png", "CloseSelected.png", CC_CALLBACK_1(HelloWorld::menuCloseCallback, this)); 2.0内的代码用的不是CC_CALLBACK_1而是menu_selector. CC_CALLBACK系列是3.

  • C++11 std::shared_ptr总结与使用示例代码详解

    最近看代码,智能指针用的比较多,自己平时用的少,周末自己总结总结.方便后续使用. std::shared_ptr大概总结有以下几点: (1) 智能指针主要的用途就是方便资源的管理,自动释放没有指针引用的资源. (2) 使用引用计数来标识是否有多余指针指向该资源.(注意,shart_ptr本身指针会占1个引用) (3) 在赋值操作中, 原来资源的引用计数会减一,新指向的资源引用计数会加一. std::shared_ptr<Test> p1(new Test); std::shared_ptr&l

  • C++11中的时间库std::chrono(引发关于时间的思考)

    前言 时间是宝贵的,我们无时无刻不在和时间打交道,这个任务明天下班前截止,你点的外卖还有5分钟才能送到,那个程序已经运行了整整48个小时,既然时间和我们联系这么紧密,我们总要定义一些术语来描述它,像前面说到的明天下班前.5分钟.48个小时都是对时间的描述,程序代码构建的程序世界也需要定义一些术语来描述时间. 今天要总结学习的是 std::chrono 库,它是 C++11 标准时从 boost 库中引入的,其实在 C++ 中还有一种 C 语言风格的时间管理体系,像我们常见的函数 time().c

  • C++11新特性std::make_tuple的使用

    std::tuple是C++ 11中引入的一个非常有用的结构,以前我们要返回一个包含不同数据类型的返回值,一般都需要自定义一个结构体或者通过函数的参数来返回,现在std::tuple就可以帮我们搞定. 1.引用头文件 #include <tuple> 2. Tuple初始化 std::tuple的初始化可以通过构造函数实现. // Creating and Initializing a tuple std::tuple<int, double, std::string> resul

  • c++ std::invalid_argument应用

    首先说明invalid_argument是一个类(class invalid_argument;),它的继承关系如下 exception-------->logic_error--------->invalid_argument invalid_argument原型是 复制代码 代码如下: class invalid_argument:public logic_error { public: explicit invalid_argument (const string& what_a

  • 利用C++实现从std::string类型到bool型的转换

    利用输入字符串流:std::istringstream 复制代码 代码如下: bool b;std::string s = "true";std::istringstream(s) >> std::boolalpha >> b; 但当字符串s为"1"时,上面的代码无法正确转换,此时应该用: 复制代码 代码如下: bool b;std::string s = "1";istringstream(s) >> b;

随机推荐