C++11 call_once 和 once_flag的使用與區(qū)別
一、簡介
std::call_once 和 std::once_flag 是 C++11 中引入的線程安全的函數(shù)和類型,用于確保某個函數(shù)只被調用一次。
std::once_flag 是一個類型,用于標記一段代碼是否已經(jīng)被執(zhí)行過。它必須通過引用傳遞給 std::call_once 函數(shù),以確保在多線程環(huán)境下僅僅執(zhí)行一次。
std::call_once 函數(shù)接受兩個參數(shù):一個可調用對象(可以是函數(shù)、lambda 表達式等)和一個 std::once_flag 對象的引用。該函數(shù)會檢查 std::once_flag 對象是否被設置過,如果沒有,就調用可調用對象,并設置 std::once_flag 對象為已設置狀態(tài)。
使用 std::call_once 和 std::once_flag 可以避免在多線程環(huán)境下多次執(zhí)行同一個函數(shù),從而提高程序性能和正確性。
下面是一個簡單的示例:
#include <iostream> #include <thread> #include <mutex> std::once_flag flag; void do_something() { ?? ?//call_once中的 lambda 表達式只執(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; }
在這個例子中,我們定義了一個名為 do_something 的函數(shù),并將其作為參數(shù)傳遞給 std::call_once 函數(shù)。 std::once_flag 對象被聲明為全局變量,以便在多個線程之間共享。
當?shù)谝淮握{用 do_something 函數(shù)時,std::call_once 將檢查 std::once_flag 是否已經(jīng)被設置過。由于初始狀態(tài)為未設置,因此 std::call_once 將執(zhí)行提供的可調用對象——這里是一個 lambda 表達式,輸出一條消息表示函數(shù)被調用了一次。
當?shù)诙握{用 do_something 函數(shù)時,std::call_once 將不再執(zhí)行提供的可調用對象,因為 std::once_flag 已經(jīng)被設置過。
通過這種方式,我們可以確保 do_something 函數(shù)中std::call_once 提供的可調用對象被調用一次,無論有多少個線程同時調用它。
/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ù),因此在多個線程同時調用時,只有一個線程會創(chuàng)建單例對象 instance_,即只有一個線程執(zhí)行函數(shù)init() ? ? //其他線程會直接返回之前創(chuàng)建的單例對象 instance_,從而保證單例對象只被創(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(); ? ? } ? ? //在類的定義中,一個靜態(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() { ? ? //調用 Singleton::getInstance() 函數(shù)來獲取單例對象的引用 ? ? 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)建多個線程并啟動它們: ? ? ? ? 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ù)來保證單例模式在多線程環(huán)境中的正確性。當?shù)谝粋€線程調用 getInstance() 函數(shù)時,會執(zhí)行 init() 函數(shù)來創(chuàng)建單例對象,同時將 flag_ 標志位設置為“已調用”。在后續(xù)的調用中,std::call_once 函數(shù)會檢查 flag_ 標志位是否已經(jīng)被設置,如果已經(jīng)被設置,則直接返回之前創(chuàng)建的單例對象,不會再次執(zhí)行 init() 函數(shù),從而保證單例對象只被創(chuàng)建一次。
在 main() 函數(shù)中,我們創(chuàng)建了多個線程,并將 thread_func 函數(shù)作為線程函數(shù),分別啟動這些線程。在 thread_func 函數(shù)中,我們調用 Singleton::getInstance() 函數(shù)來獲取單例對象的引用,并輸出它的地址。由于 getInstance() 函數(shù)使用了 std::call_once 函數(shù),因此在多個線程同時調用時,只有一個線程會創(chuàng)建單例對象,其他線程會直接返回之前創(chuàng)建的單例對象,從而保證單例對象只被創(chuàng)建一次。
使用 std::call_once 函數(shù)可以非常方便地實現(xiàn)線程安全的單例模式,通過在多個線程同時調用時只創(chuàng)建一個對象來避免資源競爭和數(shù)據(jù)不一致的問題。在多線程環(huán)境中使用單例模式時,可以將 getInstance() 函數(shù)作為線程函數(shù),在多個線程中同時調用,以驗證單例對象的創(chuàng)建情況。
示例代碼中的析構函數(shù)只會執(zhí)行一次,因為 Singleton::init函數(shù)只執(zhí)行一次:
static void init() { instance_ = new Singleton(); }
在這個例子中,Singleton 的構造函數(shù)只會執(zhí)行一次,是因為在使用 std::call_once 函數(shù)時,該函數(shù)會使用一個 std::once_flag 類型的變量來標記是否已經(jīng)執(zhí)行過初始化函數(shù),從而保證初始化函數(shù)只會被執(zhí)行一次。
具體來說,當多個線程同時調用 Singleton::getInstance() 函數(shù)時,只有其中一個線程會執(zhí)行 std::call_once 函數(shù)指定的初始化函數(shù) &Singleton::init,其他線程會阻塞等待初始化函數(shù)執(zhí)行完畢。初始化函數(shù)執(zhí)行完畢之后,所有線程都會返回之前創(chuàng)建的單例對象 instance_ 的引用,從而保證單例對象只被創(chuàng)建一次。
在這個例子中,Singleton 的構造函數(shù)在初始化函數(shù) &Singleton::init 中被調用,因此只會被執(zhí)行一次。在其他線程中,由于 instance_ 已經(jīng)被創(chuàng)建,因此不會再次調用構造函數(shù)。
備注:在C++中,類的靜態(tài)成員變量是與類相關聯(lián)的變量,而不是與對象相關聯(lián)的。它們被視為該類的所有對象共享的變量,并且只有一個副本存在于內存中。靜態(tài)成員變量通常用于跟蹤某些信息,例如,表示所有實例之間共享的計數(shù)器或全局配置設置等。
在類的定義中,一個靜態(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ù)是一個 C++ 標準庫函數(shù),它接受三個參數(shù):
(1)std::once_flag& flag:一個標志位對象的引用,用于記錄該函數(shù)是否已經(jīng)被調用過。
(2)Callable&& func:一個可調用對象,即函數(shù)或函數(shù)對象,用于執(zhí)行需要僅執(zhí)行一次的代碼。
(3)Args&&… args:可變模板參數(shù)包,用于傳遞給 func 函數(shù)的參數(shù)。
函數(shù)的實現(xiàn)分為以下步驟:
(1)創(chuàng)建一個 lambda 表達式 __callable,該表達式調用 std::__invoke 函數(shù)來執(zhí)行 __f 函數(shù)并傳遞參數(shù) __args…。
(2)創(chuàng)建一個 once_flag::_Prepare_execution 對象 __exec,該對象將在析構時執(zhí)行 __callable。
(3)調用 __gthread_once 函數(shù)來執(zhí)行一次性操作,如果操作已經(jīng)被執(zhí)行過,則不執(zhí)行。如果在執(zhí)行過程中發(fā)生異常,則不會重置 __once 標志位。
(4)如果 __gthread_once 函數(shù)返回一個非0 的值,則拋出一個系統(tǒng)錯誤異常。
下面是對代碼實現(xiàn)的詳細解釋:
template<typename _Callable, typename... _Args> void call_once(once_flag& __once, _Callable&& __f, _Args&&... __args) { ? // 創(chuàng)建一個可調用對象 __callable,該對象調用 __f 函數(shù)并傳遞參數(shù) __args... ? auto __callable = [&] { ? ? std::__invoke(std::forward<_Callable>(__f), std::forward<_Args>(__args)...); ? }; ? // 創(chuàng)建一個 __exec 對象,并在其析構時調用 __callable ? once_flag::_Prepare_execution __exec(__callable); ? // 調用 __gthread_once 函數(shù)執(zhí)行一次性操作 ? if (int __e = __gthread_once(&__once._M_once, &__once_proxy)) ? ? __throw_system_error(__e); }
在實現(xiàn)中,首先使用 lambda 表達式創(chuàng)建了一個可調用對象 __callable,該對象調用 std::__invoke 函數(shù)來執(zhí)行傳入的可調用對象 __f 并傳遞參數(shù) __args…。這個可調用對象將在后續(xù)的線程安全的執(zhí)行中使用。
接著,創(chuàng)建了一個 once_flag::_Prepare_execution 對象 __exec,該對象的構造函數(shù)接受一個可調用對象,并在其析構時調用該對象。這個對象的作用是確保在 std::call_once 函數(shù)執(zhí)行結束后,可調用對象 __callable 被正確地執(zhí)行。
然后,調用了 __gthread_once 函數(shù)來執(zhí)行一次性操作。該函數(shù)接受兩個參數(shù):一個指向 __once._M_once 變量的指針,以及一個指向 __once_proxy 函數(shù)的指針。__once._M_once 是一個原子類型的變量,用于記錄一次性操作是否已經(jīng)被執(zhí)行過。__once_proxy 函數(shù)是一個輔助函數(shù),其作用是調用 __exec 對象的可調用對象。
如果 __gthread_once 函數(shù)返回一個非0 的值,則說明執(zhí)行失敗,此時會拋出一個系統(tǒng)錯誤異常。
需要注意的是,std::call_once 函數(shù)的實現(xiàn)依賴于操作系統(tǒng)和編譯器提供的線程庫。在不同的平臺和編譯器下,__gthread_once 函數(shù)的實現(xiàn)可能有所不同。但是,無論在哪個平臺和編譯器下,std::call_once 函數(shù)都會保證傳入的可調用對象只會被執(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 結構體通過提供同步機制來確保特定任務僅在首次執(zhí)行時被執(zhí)行一次,無論有多少個線程嘗試執(zhí)行它。它通過提供一個同步機制,允許線程等待特定任務被執(zhí)行,并在任務被第一次執(zhí)行后將其標記為已完成。
once_flag 結構體具有刪除的拷貝構造函數(shù)和賦值運算符,這意味著它不能從另一個 once_flag 實例中拷貝或賦值。
在內部,once_flag 結構體包含一個 _M_once 成員變量,類型為 __gthread_once_t,它由底層線程庫(在本例中為 gthreads)用于處理目標任務的同步和執(zhí)行。
call_once 函數(shù)是 once_flag 的友元函數(shù),它接受一個 once_flag 實例以及一個目標函數(shù)和其參數(shù)。它確保目標函數(shù)僅被執(zhí)行一次,并對調用它的所有線程進行同步訪問。
其中:
std::call_once 函數(shù)需要訪問 std::once_flag 類的私有成員 _M_once,以確保可調用對象只被執(zhí)行一次。但是,將 _M_once 成員聲明為公共成員會破壞 std::once_flag 類的封裝性,而將其聲明為私有成員則無法從 std::call_once 函數(shù)中訪問。
因此,為了解決這個問題,C++ 標準庫將 std::call_once 函數(shù)聲明為 std::once_flag 類的友元函數(shù)。這樣,std::call_once 函數(shù)就可以訪問 std::once_flag 類的私有成員 _M_once,而不會破壞 std::once_flag 類的封裝性。
通過將 std::call_once 聲明為 std::once_flag 類的友元函數(shù),可以保證 std::call_once 函數(shù)與 std::once_flag 類緊密地結合在一起,形成一個可靠的只執(zhí)行一次的函數(shù)機制。同時,它也使得使用 std::call_once 函數(shù)更加方便,用戶只需要提供一個 std::once_flag 對象和一個可調用對象作為參數(shù),即可實現(xiàn)只執(zhí)行一次的函數(shù)調用。
C++ 中友元函數(shù):
在 C++ 中,友元函數(shù)是一種特殊的函數(shù),它可以訪問類的私有成員和保護成員。友元函數(shù)可以作為類的非成員函數(shù)或其他類的成員函數(shù)來聲明。在函數(shù)聲明前加上 friend 關鍵字即可將其聲明為友元函數(shù)。
友元函數(shù)對于實現(xiàn)一些特殊的功能非常有用,例如操作符重載、單例模式、只執(zhí)行一次函數(shù)等等。友元函數(shù)可以訪問類的私有成員和保護成員,這使得它們可以直接操作類的內部數(shù)據(jù),而不需要通過類的公共接口來訪問。這樣可以提高程序的效率和靈活性,同時也可以保證類的封裝性不被破壞。
需要注意的是,友元函數(shù)不是類的成員函數(shù),因此它沒有 this 指針,也不能直接訪問類的成員變量和成員函數(shù)。友元函數(shù)可以通過類的對象、指針或引用來訪問類的成員變量和成員函數(shù),或者將類的成員變量和成員函數(shù)作為參數(shù)傳遞給友元函數(shù)。
總之,友元函數(shù)是一種特殊的函數(shù),它可以訪問類的私有成員和保護成員,但不是類的成員函數(shù)。友元函數(shù)可以通過類的對象、指針或引用來訪問類的成員變量和成員函數(shù),或者將類的成員變量和成員函數(shù)作為參數(shù)傳遞給友元函數(shù)。友元函數(shù)對于實現(xiàn)一些特殊的功能非常有用,但需要謹慎使用,以避免破壞類的封裝性。
三、Linux內核中的 DO_ONCE 機制
在Linux 內核中也有對應的機制:DO_ONCE 宏。
DO_ONCE 宏是 Linux 內核中實現(xiàn)一次性代碼執(zhí)行的一種機制,可以保證多個線程同時調用宏時只有一個線程會執(zhí)行代碼,從而避免了重復執(zhí)行的問題,并且可以確保代碼的正確性和可靠性。
到此這篇關于C++11 call_once 和 once_flag的使用與區(qū)別的文章就介紹到這了,更多相關C++11 call_once once_flag內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!