std::call_once vs std::mutex for thread-safe initialization

后端 未结 3 1065
南笙
南笙 2021-02-05 10:16

I\'m a bit confused about the purpose of std::call_once. To be clear, I understand exactly what std::call_once does, and how to use it. It\'

相关标签:
3条回答
  • 2021-02-05 10:44

    Slight variation on standard C++ solution is to use lambda inside the usual one:

    // header.h
    namespace dbj_once {
    
        struct singleton final {};
    
        inline singleton & instance()
        {
            static singleton single_instance = []() -> singleton {
                // this is called only once
                // do some more complex initialization
                // here
                return {};
            }();
            return single_instance;
        };
    
     } // dbj_once
    

    Please observe

    • anonymous namespace implies default static linkage for variables inside. Thus do not put this inside it. This is header code.
    • worth repeating: this is safe in presence of multiple threads (MT) and is supported as a such by all major compilers
    • inside is a lambda which is guaranteed to be called only once
    • this pattern is also safe to use in header only situations
    0 讨论(0)
  • 2021-02-05 10:44

    If you read this you'll see that std::call_once makes no guarantee about data-races, it's simply a utility function for performing an action once (which will work across threads). You shouldn't presume that is has anything close to the affect of a mutex.

    as an example:

    #include <thread>
    #include <mutex>
    
    static std::once_flag flag;
    
    void f(){
        operation_that_takes_time();
        std::call_once(flag, [](){std::cout << "f() was called\n";});
    }
    
    void g(){
        operation_that_takes_time();
        std::call_once(flag, [](){std::cout << "g() was called\n";});
    }
    
    int main(int argc, char *argv[]){
        std::thread t1(f);
        std::thread t2(g);
        t1.join();
        t2.join();
    }
    

    could print both f() was called and g() was called. This is because in the body of std::call_once it will check whether flag was set then set it if not then call the appropriate function. But while it is checking or before it set flag another thread may call call_once with the same flag and run a function at the same time. You should still protect calls to call_once with a mutex if you know another thread may have a data race.

    EDIT

    I found a link to the proposal for the std::call_once function and thread library which states that concurrency is guaranteed to only call the function once, so it should work like a mutex (y)

    More specifically:

    If multiple calls to call_once with the same flag are executing concurrently in separate threads, then only one thread shall call func, and no thread shall proceed until the call to func has completed.

    So to answer your question: yes, other threads will be blocked until the calling thread returns from the specified functor.

    0 讨论(0)
  • 2021-02-05 11:01

    One thing that call_once does for you is handle exceptions. That is, if the first thread into it throws an exception inside of the functor (and propagates it out), call_once will not consider the call_once satisfied. A subsequent invocation is allowed to enter the functor again in an effort to complete it without an exception.

    In your example, the exceptional case is also handled properly. However it is easy to imagine a more complicated functor where the exceptional case would not be properly handled.

    All this being said, I note that call_once is redundant with function-local-statics. E.g.:

    CSingleton& CSingleton::GetInstance()
    {
        static std::unique_ptr<CSingleton> m_instance(new CSingleton);
        return *m_instance;
    }
    

    Or more simply:

    CSingleton& CSingleton::GetInstance()
    {
        static CSingleton m_instance;
        return m_instance;
    }
    

    The above is equivalent to your example with call_once, and imho, simpler. Oh, except the order of destruction is very subtly different between this and your example. In both cases m_instance is destroyed in reverse order of construction. But the order of construction is different. In your m_instance is constructed relative to other objects with file-local scope in the same translation unit. Using function-local-statics, m_instance is constructed the first time GetInstance is executed.

    That difference may or may not be important to your application. Generally I prefer the function-local-static solution as it is "lazy". I.e. if the application never calls GetInstance() then m_instance is never constructed. And there is no period during application launch when a lot of statics are trying to be constructed at once. You pay for the construction only when actually used.

    0 讨论(0)
提交回复
热议问题