• 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也是非常easy的。仅仅要传进一个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
    "; // this message may be printed from 0 to 3 times
        // if function exits via exception, another function selected
        throw std::exception();
      }
     
      std::cout << "once
    "; // 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)

    虽然代码看起来非常easy,可是要细致分析它的各种时序也比較复杂。

    有个地方比較疑惑的:

    对于同步的__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

    版权声明:本文博客原创文章,博客,未经同意,不得转载。

  • 相关阅读:
    leetcode刷题-54螺旋矩阵
    leetcode刷题-53最大子序和
    leetcode刷题-52N皇后2
    leetcode刷题-51N皇后
    leetcode刷题-50Pow(x, n)
    leetcode刷题-37解数独
    leetcode刷题-49字母异位词分组
    leetcode刷题-48旋转图像
    数据结构—B树、B+树、B*树
    LeetCode–旋转数组的最小数字
  • 原文地址:https://www.cnblogs.com/blfshiye/p/4717646.html
Copyright © 2020-2023  润新知