1. 程式人生 > >C++11中once_flag,call_once實現分析

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++ 11版本之前,static變數的初始化,並不是執行緒安全的。C++ 11版本是執行緒安全的。

可以參考:https://www.cnblogs.com/litaozijin/p/6888049.html

C++ 11版本之前比如

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


實際上相當於這樣的程式碼:

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

https://blog.csdn.net/hengyunabc/article/details/33031465