C++11 call_once 和 once_flag的使用與區(qū)別
一、簡(jiǎn)介
std::call_once 和 std::once_flag 是 C++11 中引入的線程安全的函數(shù)和類型,用于確保某個(gè)函數(shù)只被調(diào)用一次。
std::once_flag 是一個(gè)類型,用于標(biāo)記一段代碼是否已經(jīng)被執(zhí)行過。它必須通過引用傳遞給 std::call_once 函數(shù),以確保在多線程環(huán)境下僅僅執(zhí)行一次。
std::call_once 函數(shù)接受兩個(gè)參數(shù):一個(gè)可調(diào)用對(duì)象(可以是函數(shù)、lambda 表達(dá)式等)和一個(gè) std::once_flag 對(duì)象的引用。該函數(shù)會(huì)檢查 std::once_flag 對(duì)象是否被設(shè)置過,如果沒有,就調(diào)用可調(diào)用對(duì)象,并設(shè)置 std::once_flag 對(duì)象為已設(shè)置狀態(tài)。
使用 std::call_once 和 std::once_flag 可以避免在多線程環(huán)境下多次執(zhí)行同一個(gè)函數(shù),從而提高程序性能和正確性。
下面是一個(gè)簡(jiǎn)單的示例:
#include <iostream> #include <thread> #include <mutex> std::once_flag flag; void do_something() { ?? ?//call_once中的 lambda 表達(dá)式只執(zhí)行一次 ? ? std::call_once(flag, []() { ? ? ? ? std::cout << "do_something() called once" << std::endl; ? ? }); ? ? std::cout << "Thread id" << std::this_thread::get_id() << std::endl; } int main() { ? ? std::thread t1(do_something); ? ? std::thread t2(do_something); ? ? t1.join(); ? ? t2.join(); ? ? return 0; }
在這個(gè)例子中,我們定義了一個(gè)名為 do_something 的函數(shù),并將其作為參數(shù)傳遞給 std::call_once 函數(shù)。 std::once_flag 對(duì)象被聲明為全局變量,以便在多個(gè)線程之間共享。
當(dāng)?shù)谝淮握{(diào)用 do_something 函數(shù)時(shí),std::call_once 將檢查 std::once_flag 是否已經(jīng)被設(shè)置過。由于初始狀態(tài)為未設(shè)置,因此 std::call_once 將執(zhí)行提供的可調(diào)用對(duì)象——這里是一個(gè) lambda 表達(dá)式,輸出一條消息表示函數(shù)被調(diào)用了一次。
當(dāng)?shù)诙握{(diào)用 do_something 函數(shù)時(shí),std::call_once 將不再執(zhí)行提供的可調(diào)用對(duì)象,因?yàn)?std::once_flag 已經(jīng)被設(shè)置過。
通過這種方式,我們可以確保 do_something 函數(shù)中std::call_once 提供的可調(diào)用對(duì)象被調(diào)用一次,無(wú)論有多少個(gè)線程同時(shí)調(diào)用它。
/modern_c++$ ./a.out do_something() called once Thread id139891421738688 Thread id139891413345984
二、原理
2.1 示例
#include <iostream> #include <thread> #include <vector> #include <mutex> class Singleton { public: ? ? //使用了 std::call_once 函數(shù),因此在多個(gè)線程同時(shí)調(diào)用時(shí),只有一個(gè)線程會(huì)創(chuàng)建單例對(duì)象 instance_,即只有一個(gè)線程執(zhí)行函數(shù)init() ? ? //其他線程會(huì)直接返回之前創(chuàng)建的單例對(duì)象 instance_,從而保證單例對(duì)象只被創(chuàng)建一次 ? ? static Singleton& getInstance() ? ? { ? ? ? ? std::call_once(flag_, &Singleton::init); ? ? ? ? return *instance_; ? ? } ? ? Singleton(const Singleton&) = delete; ? ? Singleton& operator=(const Singleton&) = delete; private: ? ? Singleton() { std::cout << "Singleton instance created.\n"; } ? ? static void init() ? ? { ? ? ? ? instance_ = new Singleton(); ? ? } ? ? //在類的定義中,一個(gè)靜態(tài)成員變量必須由該類聲明為static ? ? //并且通常還需要在類外初始化,這意味著在類的定義中僅指定其類型和名稱 ? ? static std::once_flag flag_; ? ? static Singleton* instance_; }; //在 class 外初始化 static 成員變量 std::once_flag Singleton::flag_; Singleton* Singleton::instance_ = nullptr; void thread_func() { ? ? //調(diào)用 Singleton::getInstance() 函數(shù)來(lái)獲取單例對(duì)象的引用 ? ? Singleton& singleton = Singleton::getInstance(); ? ? std::cout << "Singleton instance address: " << &singleton << "\n"; } int main() { ? ? std::vector<std::thread> threads; ? ? const int num_threads = 10; ? ? for (int i = 0; i < num_threads; ++i) ? ? { ? ? ? ? //threads將 `thread_func` 函數(shù)作為線程函數(shù),創(chuàng)建多個(gè)線程并啟動(dòng)它們: ? ? ? ? threads.emplace_back(thread_func); ? ? } ? ? for (auto& t : threads) ? ? { ? ? ? ? t.join(); ? ? } ? ? return 0; }
modern_c++$ ./a.out Singleton instance created. Singleton instance address: 0x7f1310000b70 Singleton instance address: 0x7f1310000b70 Singleton instance address: 0x7f1310000b70 Singleton instance address: 0x7f1310000b70 Singleton instance address: 0x7f1310000b70 Singleton instance address: 0x7f1310000b70 Singleton instance address: 0x7f1310000b70 Singleton instance address: 0x7f1310000b70 Singleton instance address: 0x7f1310000b70 Singleton instance address: 0x7f1310000b70
在上面的代碼中,我們使用 std::call_once 函數(shù)來(lái)保證單例模式在多線程環(huán)境中的正確性。當(dāng)?shù)谝粋€(gè)線程調(diào)用 getInstance() 函數(shù)時(shí),會(huì)執(zhí)行 init() 函數(shù)來(lái)創(chuàng)建單例對(duì)象,同時(shí)將 flag_ 標(biāo)志位設(shè)置為“已調(diào)用”。在后續(xù)的調(diào)用中,std::call_once 函數(shù)會(huì)檢查 flag_ 標(biāo)志位是否已經(jīng)被設(shè)置,如果已經(jīng)被設(shè)置,則直接返回之前創(chuàng)建的單例對(duì)象,不會(huì)再次執(zhí)行 init() 函數(shù),從而保證單例對(duì)象只被創(chuàng)建一次。
在 main() 函數(shù)中,我們創(chuàng)建了多個(gè)線程,并將 thread_func 函數(shù)作為線程函數(shù),分別啟動(dòng)這些線程。在 thread_func 函數(shù)中,我們調(diào)用 Singleton::getInstance() 函數(shù)來(lái)獲取單例對(duì)象的引用,并輸出它的地址。由于 getInstance() 函數(shù)使用了 std::call_once 函數(shù),因此在多個(gè)線程同時(shí)調(diào)用時(shí),只有一個(gè)線程會(huì)創(chuàng)建單例對(duì)象,其他線程會(huì)直接返回之前創(chuàng)建的單例對(duì)象,從而保證單例對(duì)象只被創(chuàng)建一次。
使用 std::call_once 函數(shù)可以非常方便地實(shí)現(xiàn)線程安全的單例模式,通過在多個(gè)線程同時(shí)調(diào)用時(shí)只創(chuàng)建一個(gè)對(duì)象來(lái)避免資源競(jìng)爭(zhēng)和數(shù)據(jù)不一致的問題。在多線程環(huán)境中使用單例模式時(shí),可以將 getInstance() 函數(shù)作為線程函數(shù),在多個(gè)線程中同時(shí)調(diào)用,以驗(yàn)證單例對(duì)象的創(chuàng)建情況。
示例代碼中的析構(gòu)函數(shù)只會(huì)執(zhí)行一次,因?yàn)?Singleton::init函數(shù)只執(zhí)行一次:
static void init() { instance_ = new Singleton(); }
在這個(gè)例子中,Singleton 的構(gòu)造函數(shù)只會(huì)執(zhí)行一次,是因?yàn)樵谑褂?std::call_once 函數(shù)時(shí),該函數(shù)會(huì)使用一個(gè) std::once_flag 類型的變量來(lái)標(biāo)記是否已經(jīng)執(zhí)行過初始化函數(shù),從而保證初始化函數(shù)只會(huì)被執(zhí)行一次。
具體來(lái)說,當(dāng)多個(gè)線程同時(shí)調(diào)用 Singleton::getInstance() 函數(shù)時(shí),只有其中一個(gè)線程會(huì)執(zhí)行 std::call_once 函數(shù)指定的初始化函數(shù) &Singleton::init,其他線程會(huì)阻塞等待初始化函數(shù)執(zhí)行完畢。初始化函數(shù)執(zhí)行完畢之后,所有線程都會(huì)返回之前創(chuàng)建的單例對(duì)象 instance_ 的引用,從而保證單例對(duì)象只被創(chuàng)建一次。
在這個(gè)例子中,Singleton 的構(gòu)造函數(shù)在初始化函數(shù) &Singleton::init 中被調(diào)用,因此只會(huì)被執(zhí)行一次。在其他線程中,由于 instance_ 已經(jīng)被創(chuàng)建,因此不會(huì)再次調(diào)用構(gòu)造函數(shù)。
備注:在C++中,類的靜態(tài)成員變量是與類相關(guān)聯(lián)的變量,而不是與對(duì)象相關(guān)聯(lián)的。它們被視為該類的所有對(duì)象共享的變量,并且只有一個(gè)副本存在于內(nèi)存中。靜態(tài)成員變量通常用于跟蹤某些信息,例如,表示所有實(shí)例之間共享的計(jì)數(shù)器或全局配置設(shè)置等。
在類的定義中,一個(gè)靜態(tài)成員變量必須由該類聲明為static,并且通常還需要在類外初始化,這意味著在類的定義中僅指定其類型和名稱。
2.2 call_once源碼詳解
? template<typename _Callable, typename... _Args> ? ? void ? ? call_once(once_flag& __once, _Callable&& __f, _Args&&... __args) ? ? { ? ? ? // Closure type that runs the function ? ? ? auto __callable = [&] { ?? ? ?std::__invoke(std::forward<_Callable>(__f), ?? ??? ??? ?std::forward<_Args>(__args)...); ? ? ? }; ? ? ? once_flag::_Prepare_execution __exec(__callable); ? ? ? // XXX pthread_once does not reset the flag if an exception is thrown. ? ? ? if (int __e = __gthread_once(&__once._M_once, &__once_proxy)) ?? ?__throw_system_error(__e); ? ? }
std::call_once 函數(shù)是一個(gè) C++ 標(biāo)準(zhǔn)庫(kù)函數(shù),它接受三個(gè)參數(shù):
(1)std::once_flag& flag:一個(gè)標(biāo)志位對(duì)象的引用,用于記錄該函數(shù)是否已經(jīng)被調(diào)用過。
(2)Callable&& func:一個(gè)可調(diào)用對(duì)象,即函數(shù)或函數(shù)對(duì)象,用于執(zhí)行需要僅執(zhí)行一次的代碼。
(3)Args&&… args:可變模板參數(shù)包,用于傳遞給 func 函數(shù)的參數(shù)。
函數(shù)的實(shí)現(xiàn)分為以下步驟:
(1)創(chuàng)建一個(gè) lambda 表達(dá)式 __callable,該表達(dá)式調(diào)用 std::__invoke 函數(shù)來(lái)執(zhí)行 __f 函數(shù)并傳遞參數(shù) __args…。
(2)創(chuàng)建一個(gè) once_flag::_Prepare_execution 對(duì)象 __exec,該對(duì)象將在析構(gòu)時(shí)執(zhí)行 __callable。
(3)調(diào)用 __gthread_once 函數(shù)來(lái)執(zhí)行一次性操作,如果操作已經(jīng)被執(zhí)行過,則不執(zhí)行。如果在執(zhí)行過程中發(fā)生異常,則不會(huì)重置 __once 標(biāo)志位。
(4)如果 __gthread_once 函數(shù)返回一個(gè)非0 的值,則拋出一個(gè)系統(tǒng)錯(cuò)誤異常。
下面是對(duì)代碼實(shí)現(xiàn)的詳細(xì)解釋:
template<typename _Callable, typename... _Args> void call_once(once_flag& __once, _Callable&& __f, _Args&&... __args) { ? // 創(chuàng)建一個(gè)可調(diào)用對(duì)象 __callable,該對(duì)象調(diào)用 __f 函數(shù)并傳遞參數(shù) __args... ? auto __callable = [&] { ? ? std::__invoke(std::forward<_Callable>(__f), std::forward<_Args>(__args)...); ? }; ? // 創(chuàng)建一個(gè) __exec 對(duì)象,并在其析構(gòu)時(shí)調(diào)用 __callable ? once_flag::_Prepare_execution __exec(__callable); ? // 調(diào)用 __gthread_once 函數(shù)執(zhí)行一次性操作 ? if (int __e = __gthread_once(&__once._M_once, &__once_proxy)) ? ? __throw_system_error(__e); }
在實(shí)現(xiàn)中,首先使用 lambda 表達(dá)式創(chuàng)建了一個(gè)可調(diào)用對(duì)象 __callable,該對(duì)象調(diào)用 std::__invoke 函數(shù)來(lái)執(zhí)行傳入的可調(diào)用對(duì)象 __f 并傳遞參數(shù) __args…。這個(gè)可調(diào)用對(duì)象將在后續(xù)的線程安全的執(zhí)行中使用。
接著,創(chuàng)建了一個(gè) once_flag::_Prepare_execution 對(duì)象 __exec,該對(duì)象的構(gòu)造函數(shù)接受一個(gè)可調(diào)用對(duì)象,并在其析構(gòu)時(shí)調(diào)用該對(duì)象。這個(gè)對(duì)象的作用是確保在 std::call_once 函數(shù)執(zhí)行結(jié)束后,可調(diào)用對(duì)象 __callable 被正確地執(zhí)行。
然后,調(diào)用了 __gthread_once 函數(shù)來(lái)執(zhí)行一次性操作。該函數(shù)接受兩個(gè)參數(shù):一個(gè)指向 __once._M_once 變量的指針,以及一個(gè)指向 __once_proxy 函數(shù)的指針。__once._M_once 是一個(gè)原子類型的變量,用于記錄一次性操作是否已經(jīng)被執(zhí)行過。__once_proxy 函數(shù)是一個(gè)輔助函數(shù),其作用是調(diào)用 __exec 對(duì)象的可調(diào)用對(duì)象。
如果 __gthread_once 函數(shù)返回一個(gè)非0 的值,則說明執(zhí)行失敗,此時(shí)會(huì)拋出一個(gè)系統(tǒng)錯(cuò)誤異常。
需要注意的是,std::call_once 函數(shù)的實(shí)現(xiàn)依賴于操作系統(tǒng)和編譯器提供的線程庫(kù)。在不同的平臺(tái)和編譯器下,__gthread_once 函數(shù)的實(shí)現(xiàn)可能有所不同。但是,無(wú)論在哪個(gè)平臺(tái)和編譯器下,std::call_once 函數(shù)都會(huì)保證傳入的可調(diào)用對(duì)象只會(huì)被執(zhí)行一次。
2.3 once_flag源碼詳解
? /// Flag type used by std::call_once ? struct once_flag ? { ? ? constexpr once_flag() noexcept = default; ? ? /// Deleted copy constructor ? ? once_flag(const once_flag&) = delete; ? ? /// Deleted assignment operator ? ? once_flag& operator=(const once_flag&) = delete; ? private: ? ? // For gthreads targets a pthread_once_t is used with pthread_once, but ? ? // for most targets this doesn't work correctly for exceptional executions. ? ? __gthread_once_t _M_once = __GTHREAD_ONCE_INIT; ? ? struct _Prepare_execution; ? ? template<typename _Callable, typename... _Args> ? ? ? friend void ? ? ? call_once(once_flag& __once, _Callable&& __f, _Args&&... __args); ? };
once_flag 結(jié)構(gòu)體通過提供同步機(jī)制來(lái)確保特定任務(wù)僅在首次執(zhí)行時(shí)被執(zhí)行一次,無(wú)論有多少個(gè)線程嘗試執(zhí)行它。它通過提供一個(gè)同步機(jī)制,允許線程等待特定任務(wù)被執(zhí)行,并在任務(wù)被第一次執(zhí)行后將其標(biāo)記為已完成。
once_flag 結(jié)構(gòu)體具有刪除的拷貝構(gòu)造函數(shù)和賦值運(yùn)算符,這意味著它不能從另一個(gè) once_flag 實(shí)例中拷貝或賦值。
在內(nèi)部,once_flag 結(jié)構(gòu)體包含一個(gè) _M_once 成員變量,類型為 __gthread_once_t,它由底層線程庫(kù)(在本例中為 gthreads)用于處理目標(biāo)任務(wù)的同步和執(zhí)行。
call_once 函數(shù)是 once_flag 的友元函數(shù),它接受一個(gè) once_flag 實(shí)例以及一個(gè)目標(biāo)函數(shù)和其參數(shù)。它確保目標(biāo)函數(shù)僅被執(zhí)行一次,并對(duì)調(diào)用它的所有線程進(jìn)行同步訪問。
其中:
std::call_once 函數(shù)需要訪問 std::once_flag 類的私有成員 _M_once,以確??烧{(diào)用對(duì)象只被執(zhí)行一次。但是,將 _M_once 成員聲明為公共成員會(huì)破壞 std::once_flag 類的封裝性,而將其聲明為私有成員則無(wú)法從 std::call_once 函數(shù)中訪問。
因此,為了解決這個(gè)問題,C++ 標(biāo)準(zhǔn)庫(kù)將 std::call_once 函數(shù)聲明為 std::once_flag 類的友元函數(shù)。這樣,std::call_once 函數(shù)就可以訪問 std::once_flag 類的私有成員 _M_once,而不會(huì)破壞 std::once_flag 類的封裝性。
通過將 std::call_once 聲明為 std::once_flag 類的友元函數(shù),可以保證 std::call_once 函數(shù)與 std::once_flag 類緊密地結(jié)合在一起,形成一個(gè)可靠的只執(zhí)行一次的函數(shù)機(jī)制。同時(shí),它也使得使用 std::call_once 函數(shù)更加方便,用戶只需要提供一個(gè) std::once_flag 對(duì)象和一個(gè)可調(diào)用對(duì)象作為參數(shù),即可實(shí)現(xiàn)只執(zhí)行一次的函數(shù)調(diào)用。
C++ 中友元函數(shù):
在 C++ 中,友元函數(shù)是一種特殊的函數(shù),它可以訪問類的私有成員和保護(hù)成員。友元函數(shù)可以作為類的非成員函數(shù)或其他類的成員函數(shù)來(lái)聲明。在函數(shù)聲明前加上 friend 關(guān)鍵字即可將其聲明為友元函數(shù)。
友元函數(shù)對(duì)于實(shí)現(xiàn)一些特殊的功能非常有用,例如操作符重載、單例模式、只執(zhí)行一次函數(shù)等等。友元函數(shù)可以訪問類的私有成員和保護(hù)成員,這使得它們可以直接操作類的內(nèi)部數(shù)據(jù),而不需要通過類的公共接口來(lái)訪問。這樣可以提高程序的效率和靈活性,同時(shí)也可以保證類的封裝性不被破壞。
需要注意的是,友元函數(shù)不是類的成員函數(shù),因此它沒有 this 指針,也不能直接訪問類的成員變量和成員函數(shù)。友元函數(shù)可以通過類的對(duì)象、指針或引用來(lái)訪問類的成員變量和成員函數(shù),或者將類的成員變量和成員函數(shù)作為參數(shù)傳遞給友元函數(shù)。
總之,友元函數(shù)是一種特殊的函數(shù),它可以訪問類的私有成員和保護(hù)成員,但不是類的成員函數(shù)。友元函數(shù)可以通過類的對(duì)象、指針或引用來(lái)訪問類的成員變量和成員函數(shù),或者將類的成員變量和成員函數(shù)作為參數(shù)傳遞給友元函數(shù)。友元函數(shù)對(duì)于實(shí)現(xiàn)一些特殊的功能非常有用,但需要謹(jǐn)慎使用,以避免破壞類的封裝性。
三、Linux內(nèi)核中的 DO_ONCE 機(jī)制
在Linux 內(nèi)核中也有對(duì)應(yīng)的機(jī)制:DO_ONCE 宏。
DO_ONCE 宏是 Linux 內(nèi)核中實(shí)現(xiàn)一次性代碼執(zhí)行的一種機(jī)制,可以保證多個(gè)線程同時(shí)調(diào)用宏時(shí)只有一個(gè)線程會(huì)執(zhí)行代碼,從而避免了重復(fù)執(zhí)行的問題,并且可以確保代碼的正確性和可靠性。
到此這篇關(guān)于C++11 call_once 和 once_flag的使用與區(qū)別的文章就介紹到這了,更多相關(guān)C++11 call_once once_flag內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
從c++標(biāo)準(zhǔn)庫(kù)指針萃取器談一下traits技法(推薦)
本篇文章基于gcc中標(biāo)準(zhǔn)庫(kù)源碼剖析一下標(biāo)準(zhǔn)庫(kù)中的模板類pointer_traits,并且以此為例理解一下traits技法,對(duì)c++ traits技法源碼分析感興趣的朋友跟隨小編一起看看吧2021-07-07輸出1000以內(nèi)的素?cái)?shù)的算法(實(shí)例代碼)
本篇文章是對(duì)輸出1000以內(nèi)的素?cái)?shù)的算法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05C語(yǔ)言中strcmp的實(shí)現(xiàn)原型
這篇文章主要介紹了C語(yǔ)言中strcmp的實(shí)現(xiàn)原型的相關(guān)資料,這里提供實(shí)例幫助大家理解這部分內(nèi)容,希望能幫助到大家,需要的朋友可以參考下2017-08-08