C++11中once_flag,call_once實現分析__C++

來源:互聯網
上載者:User

本文的分析基於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 = 0void 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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.