C++11中once_flag,call_once实现分析

本文的分析基于llvm的libc++,而不是gun的libstdc++,因为libstdc++的代码里太多宏了,看起来蛋疼。

在多线程编程中,有一个常见的情景是某个任务只需要执行一次。在C++11中提供了很方便的辅助类once_flag,call_once。

声明

首先来看一下once_flag和call_once的声明:

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是不允许修改的,拷贝构造函数和operator=函数都声明为delete,这样防止程序员乱用。

另外,call_once也是很简单的,只要传进一个once_flag,回调函数,和参数列表就可以了。

示例

看一个示例:

http://en.cppreference.com/w/cpp/thread/call_once

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

std::once_flag flag;

void do_once()
{
    std::call_once(flag, [](){ std::cout << "Called once" << std::endl; });
}

int main()
{
    std::thread t1(do_once);
    std::thread t2(do_once);
    std::thread t3(do_once);
    std::thread t4(do_once);

    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

保存为main.cpp,如果是用g++或者clang++来编绎:

g++ -std=c++11 -pthread main.cpp

clang++ -std=c++11 -pthread main.cpp

./a.out

可以看到,只会输出一行

Called once

值得注意的是,如果在函数执行中抛出了异常,那么会有另一个在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();
}

输出的结果可能是0到3行throw,和一行once。

实际上once_flag相当于一个锁,使用它的线程都会在上面等待,只有一个线程允许执行。如果该线程抛出异常,那么从等待中的线程中选择一个,重复上面的流程。

实现分析

once_flag实际上只有一个unsigned long __state_的成员变量,把call_once声明为友元函数,这样call_once能修改__state__变量:

struct once_flag
{
        once_flag() _NOEXCEPT : __state_(0) {}
private:
    once_flag(const once_flag&); // = delete;
    once_flag& operator=(const once_flag&); // = delete;

    unsigned long __state_;

    template<class _Callable>
    friend void call_once(once_flag&, _Callable);
};

call_once则用了一个__call_once_param类来包装函数,很常见的模板编程技巧。

template <class _Fp>
class __call_once_param
{
    _Fp __f_;
public:
    explicit __call_once_param(const _Fp& __f) : __f_(__f) {}
    void operator()()
    {
        __f_();
    }
};
template<class _Callable>
void call_once(once_flag& __flag, _Callable __func)
{
    if (__flag.__state_ != ~0ul)
    {
        __call_once_param<_Callable> __p(__func);
        __call_once(__flag.__state_, &__p, &__call_once_proxy<_Callable>);
    }
}

最重要的是__call_once函数的实现:

static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t  cv  = PTHREAD_COND_INITIALIZER;

void
__call_once(volatile unsigned long& flag, void* arg, void(*func)(void*))
{
    pthread_mutex_lock(&mut);
    while (flag == 1)
        pthread_cond_wait(&cv, &mut);
    if (flag == 0)
    {
#ifndef _LIBCPP_NO_EXCEPTIONS
        try
        {
#endif  // _LIBCPP_NO_EXCEPTIONS
            flag = 1;
            pthread_mutex_unlock(&mut);
            func(arg);
            pthread_mutex_lock(&mut);
            flag = ~0ul;
            pthread_mutex_unlock(&mut);
            pthread_cond_broadcast(&cv);
#ifndef _LIBCPP_NO_EXCEPTIONS
        }
        catch (...)
        {
            pthread_mutex_lock(&mut);
            flag = 0ul;
            pthread_mutex_unlock(&mut);
            pthread_cond_broadcast(&cv);
            throw;
        }
#endif  // _LIBCPP_NO_EXCEPTIONS
    }
    else
        pthread_mutex_unlock(&mut);
}

里面用了全局的mutex和condition来做同步,还有异常处理的代码。

其实当看到mutext和condition时,就明白是如何实现的了。里面有一系列的同步操作,可以参考另外一篇blog:

http://blog.csdn.net/hengyunabc/article/details/27969613   并行编程之条件变量(posix condition variables)

尽管代码看起来很简单,但是要仔细分析它的各种时序也比较复杂。

有个地方比较疑惑的:

对于同步的__state__变量,并没有任何的memory order的保护,会不会有问题?

因为在JDK的代码里LockSupport和逻辑和上面的__call_once函数类似,但是却有memory order相关的代码:

OrderAccess::fence();

其它的东东:

有个东东值得提一下,在C++中,static变量的初始化,并不是线程安全的。

比如

void func(){
    static int value = 100;
    ...
}

实际上相当于这样的代码:

i

nt __flag = 0
void func(){
    static int value;
    if(!__flag){
        value = 100;
        __flag = 1;
    }
    ...
}

总结:

还有一件事情要考虑:所有的once_flag和call_once都共用全局的mutex和condition会不会有性能问题?

首先,像call_once这样的需求在一个程序里不会太多。另外,临界区的代码是比较很少的,只有判断各自的flag的代码。

如果有上百上千个线程在等待once_flag,那么pthread_cond_broadcast可能会造成“惊群”效果,但是如果有那么多的线程都上等待,显然程序设计有问题。

还有一个要注意的地方是once_flag的生命周期,它必须要比使用它的线程的生命周期要长。所以通常定义成全局变量比较好。

参考:

http://libcxx.llvm.org/

http://en.cppreference.com/w/cpp/thread/once_flag

http://en.cppreference.com/w/cpp/thread/call_once

C++11中once_flag,call_once实现分析,布布扣,bubuko.com

时间: 2024-11-16 00:51:39

C++11中once_flag,call_once实现分析的相关文章

C++11中的mutex, lock,condition variable实现分析

本文分析的是llvm libc++的实现:http://libcxx.llvm.org/ C++11中的各种mutex, lock对象,实际上都是对posix的mutex,condition的封装.不过里面也有很多细节值得学习. std::mutex 先来看下std::mutex: 包增了一个pthread_mutex_t __m_,很简单,每个函数该干嘛就干嘛. class mutex { pthread_mutex_t __m_; public: mutex() _NOEXCEPT {__m

【转载】C++ 11中的右值引用

本篇随笔为转载,原博地址如下:http://www.cnblogs.com/TianFang/archive/2013/01/26/2878356.html 右值引用的功能 首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能: #include <iostream>    #include <vector>    using namespace std; class obj    {    public :        obj() { cout <&l

C++ 11 中的右值引用

C++ 11 中的右值引用 右值引用的功能 首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能: #include <iostream>    #include <vector>    using namespace std; class obj    {    public :        obj() { cout << ">> create obj " << endl; }        obj(c

正则表达式简介及在C++11中的简单使用

正则表达式(regular expression)是计算机科学中的一个概念,又称规则表达式,通常简写为regex.regexp.RE.regexps.regexes.regexen. 正则表达式是一种文本模式.正则表达式是强大.便捷.高效的文本处理工具.正则表达式本身,加上如同一门袖珍编程语言的通用模式表示法(general pattern notation),赋予使用者描述和分析文本的能力.配合上特定工具提供的额外支持,正则表达式能够添加.删除.分离.叠加.插入和修整各种类型的文本和数据. 完

从linux0.11中起动部分代码看汇编调用c语言函数

上一篇分析了c语言的函数调用栈情况,知道了c语言的函数调用机制后,我们来看一下,linux0.11中起动部分的代码是如何从汇编跳入c语言函数的.在LINUX 0.11中的head.s文件中会看到如下一段代码(linux0.11的启动分析部分会在另一部分中再分析,由于此文仅涉及c与汇编代码的问题,). after_page_tables: pushl $0 # These are the parameters to main :-) pushl $0 pushl $0 pushl $L6 # re

C++11系列之——临时对象分析

/*C++中返回一个对象时的实现及传说中的右值——临时对象*/ 如下代码: 1 /**********************************************/ 2 class CStudent; 3 CStudent GetStudent() 4 { 5 CStudent loc_stu; 6 return loc_stu; 7 } 8 9 int main() 10 { 11 CStudent stu = GetStudent(); 12 } 13 /************

YII用户注冊和用户登录(三)之模型中规则制定和分析

3 模型中规则制定和分析 YII模型主要分为两类,一个数据模型,处理和数据库相关的增删改查.继承CActiveRecord.还有一个是表单模型,继承CFormModel.不与数据库进行交互.操作与数据模型保持一致. 先分析数据模型: 数据模型基本包括四个方法.这里有两个须要注意的地方: 1 还记得视图中有确认password选项吗?数据库中没有这一个字段,不进行处理的话.程序会报错 处理方法:声明这样变量,并将这个变量进行label映射 例如以下: public $password2; // /

python案例:金融营销活动中欺诈用户行为分析

下午学习了python数据分析的应用案例---金融营销活动中欺诈用户行为分析.数据来源于DC竞赛数据:https://www.dcjingsai.com/common/cmpt/2018%E5%B9%B4%E7%94%9C%E6%A9%99%E9%87%91%E8%9E%8D%E6%9D%AF%E5%A4%A7%E6%95%B0%E6%8D%AE%E5%BB%BA%E6%A8%A1%E5%A4%A7%E8%B5%9B_%E7%AB%9E%E8%B5%9B%E4%BF%A1%E6%81%AF.ht

C++11中多线程库

一.linux 线程同步 线程是在操作系统层面支持的,所以多线程的学习建议还是先找一本linux系统编程类的书,了解linux提供多线程的API.完全完全使用系统调用编写多线程程序是痛苦,现在也有很多封装好的多线程库,但是了解多线程系统对学习编写多线程程序非常有好处.总的来说linux提供了四类系统用于多程序程序,分别线程的创建.销毁(thread),用于线程同步的(互斥量(mutex).条件量(cond),信号量(sem)). 互斥量通过锁的机制实现线程间的同步.互斥量是一种特殊的变量,可以对