自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

解開C++之call_once的神秘面紗:記一個有意思的問題筆記

開發(fā) 前端
最近因為項目要求用C++,項目中需要一個接口只調(diào)用一次,需要使用到C++的call_once機制,于是寫一個小demo來測試,就因為這個足夠小發(fā)現(xiàn)了一個非常有意思的問題。

引言

最近因為項目要求用c++,之前一直很討厭c++,沒辦法只能短時間彌補c++的知識,項目中需要一個接口只調(diào)用一次,需要使用到c++的call_once機制,于是寫一個小demo來測試,就因為這個足夠小發(fā)現(xiàn)了一個非常有意思的問題。

call_once,基本原理

std::call_once 的內(nèi)部實現(xiàn)基于兩個重要的組件:std::once_flag 和 std::invoke。std::once_flag 是一個標志,用于表示某個函數(shù)是否已經(jīng)被調(diào)用過。而 std::invoke 則負責(zé)實際調(diào)用該函數(shù)。

call_once的基本工作原理是:使用 std::once_flag 來標記函數(shù)是否被調(diào)用過。當(dāng)有多個線程試圖調(diào)用 std::call_once 時,只有一個線程會執(zhí)行函數(shù),其他線程會被阻塞直至該函數(shù)執(zhí)行完畢。

std::call_once 的使用步驟三步曲:

  • 創(chuàng)建 std::once_flag 對象:在需要保證函數(shù)只調(diào)用一次的地方創(chuàng)建一個 std::once_flag 對象。
  • 編寫需要執(zhí)行一次的函數(shù):編寫你想要確保只調(diào)用一次的函數(shù)。
  • 調(diào)用 std::call_once:在需要執(zhí)行該函數(shù)的地方調(diào)用 std::call_once 并傳入 std::once_flag 和函數(shù)名稱。

demo問題引入

demo非常簡單,實現(xiàn)一個Init函數(shù)進行call_once調(diào)用,只調(diào)用一次的函數(shù)Initialize做一次打印處理,main中連續(xù)調(diào)用Init 4次,理論上來說我們執(zhí)行結(jié)果只有一行打印,這也是我們的目的。

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>

std::once_flag flag;

void Initialize()
{
        std::cout << "Run into Initialize.." << std::endl;
}

void Init()
{
        std::call_once(flag, Initialize);
}

int main(){
    Init();
    Init();
    Init();
    Init();

    return 0;
}

使用g++編譯,執(zhí)行結(jié)果發(fā)現(xiàn)出錯了:

拋出了個異常,從call_once上的理解來說代碼實現(xiàn)應(yīng)該是沒問題的。于是使用調(diào)試大法gdb,編譯+g后使用gdb調(diào)試發(fā)現(xiàn)了個有意思的:

使用gdb調(diào)試發(fā)現(xiàn)__gthread_active_ptr指針是0,然后繼續(xù)執(zhí)行發(fā)現(xiàn)___gthread_once返回的__e為0,于是繼續(xù)執(zhí)行if就拋了異常。__gthread_active_ptr這又是什么呢?

深入研究研究

怎么看呢?既然不知道是什么我一般的操作是先看預(yù)處理部分代碼,使用gcc -E參數(shù)來編譯出call_once.i文件。

g++ -E call_once.cpp -o call_once.i

打開call_once.i文件我發(fā)現(xiàn)main函數(shù)部分沒有什么特別之處,我們搜索call_once可以看到它的實現(xiàn)。

這段代碼中的 std::call_once 函數(shù)首先創(chuàng)建了一個可調(diào)用對象 __callable,這個對象會調(diào)用傳入的函數(shù) __f,并傳入 __args 參數(shù)。然后,它將 __callable 的地址存儲到 __once_callable 變量中。

接下來,通過一個 lambda 表達式將 __callable 的調(diào)用封裝在 __once_call 中,這個 lambda 表達式會執(zhí)行 __callable。

最后,使用底層線程庫的 __gthread_once 函數(shù)來確保 __once_call 只會執(zhí)行一次,即保證傳入的函數(shù) __f 只會被調(diào)用一次。

如果 __gthread_once 的返回值不為零,表示執(zhí)行出現(xiàn)了錯誤,會通過 __throw_system_error 拋出系統(tǒng)錯誤。

既然是if(__e)后拋的異常,我們繼續(xù)看__gthread_once的實現(xiàn),搜索__gthread_once關(guān)鍵字,找到其實現(xiàn):

 11452 static inline int
 11453 __gthread_once (__gthread_once_t *__once, void (*__func) (void))
 11454 {
 11455   if (__gthread_active_p ())
 11456     return __gthrw_pthread_once (__once, __func);
 11457   else
 11458     return -1;
 11459 }

這個函數(shù)可以看到執(zhí)行了__gthread_active_p ,我們繼續(xù)找__gthread_active_p 的實現(xiàn)。

__gthread_active_p 是一個內(nèi)聯(lián)函數(shù),返回一個整數(shù)值。

static void const __gthread_active_ptr 是一個靜態(tài)指針常量,初始化為 __gthrw___pthread_key_create 函數(shù)的地址。

extension 是一個 GNU C 擴展,用于告知編譯器避免對某些表達式進行警告。在此處,它將地址轉(zhuǎn)換為 void類型,以避免類型不匹配的警告。

&__gthrw___pthread_key_create 可能是一個特定線程庫(如 POSIX 線程庫)內(nèi)部的函數(shù),用于創(chuàng)建線程特定數(shù)據(jù)的關(guān)鍵字。

函數(shù)返回 __gthread_active_ptr != 0,即如果該指針非空,則表明線程已激活(從指針命名上猜的)。

所以該函數(shù)用于指示線程是否被激活。不明白?我們繼續(xù)看__gthrw___pthread_key_create的定義。

11405 static __typeof(pthread_key_create) __gthrw___pthread_key_create __attribute__ ((__weakref__("__pthread_key_create")));

通過 attribute((weakref("__pthread_key_create"))),將 __gthrw___pthread_key_create 弱引用到 __pthread_key_create。

弱引用是一種機制,允許在鏈接過程中,如果存在 __pthread_key_create 的定義,則使用它。但如果找不到 __pthread_key_create,則允許 __gthrw___pthread_key_create 仍然存在,只不過它將保持為空或未定義狀態(tài)。

這里大致就明白了,總結(jié)一下,call_once內(nèi)部實現(xiàn)中要找一個__pthread_key_create定義,如果不存在則返回空,即我們調(diào)試的時的指針給了0,從而引起異常。__pthread_key_create函數(shù)很明顯是線程函數(shù)。好,那我們在代碼中加上該函數(shù)調(diào)用試試看。在main開始增加如下:

int main(){
    pthread_key_t key;
    pthread_key_create(&key, NULL);
    Init();

編譯運行:

達到預(yù)期效果了!

所以整體分析一下,在 call_once 的內(nèi)部實現(xiàn)中,檢測是否支持 __pthread_key_create 可能是為了確保在使用 call_once 進行線程同步時,能夠利用線程特定數(shù)據(jù)鍵來管理狀態(tài)或資源,確保其正確性和性能。如果當(dāng)前環(huán)境不支持 __pthread_key_create,那么在多線程環(huán)境下可能無法有效地管理線程特定的狀態(tài)信息。

因此,檢測是否支持 __pthread_key_create 可能是 call_once 實現(xiàn)中的一種策略,用于在可能的情況下提供更好的線程安全性和性能。如果當(dāng)前環(huán)境不支持這個特定的功能,可能會采用其他方式來實現(xiàn) call_once,或者簡化其行為以確保程序在這樣的環(huán)境中仍能正確運行,盡管可能會犧牲一些特定的功能或性能。

我嘗試不使用__pthread_key_create,隨便調(diào)用一個pthread庫的api,比如pthread_create,或者pthread_mutex_init,調(diào)用可以全部傳遞空指針,結(jié)果依然是可以達到預(yù)期的。所以驗證也說明call_once內(nèi)部通過弱引用庫函數(shù)來檢測當(dāng)前是否支持多線程,如果不支持則拋出異常,所以使用前提必須是多線程環(huán)境。

總結(jié)

call_once 的魅力與注意事項:

std::call_once 提供了一種簡單而又強大的多線程同步方式,但在使用時也需注意一些細節(jié)。比如一定要確保程序是多線程調(diào)用,如果有多線程自然還要確保線程安全,避免潛在的死鎖和競態(tài)條件問題登。

責(zé)任編輯:趙寧寧 來源: 囧囧妹
相關(guān)推薦

2020-12-12 13:50:16

云開發(fā)

2021-01-27 13:54:05

開發(fā)云原生工具

2018-06-24 16:39:28

Tomcat異常線程

2024-05-20 01:10:00

Promise變量

2009-08-26 17:53:31

C# DropDown

2023-05-15 09:16:18

CSSCSS Mask

2011-06-22 09:43:01

C++

2024-03-18 08:14:07

SpringDAOAppConfig

2020-03-10 14:59:16

oracle數(shù)據(jù)庫監(jiān)聽異常

2022-03-21 10:21:50

jQuery代碼模式

2021-03-25 06:12:55

SVG 濾鏡CSS

2009-02-16 19:33:09

2012-05-22 10:12:59

jQuery

2022-08-15 22:34:47

Overflow方向裁切

2022-06-15 07:21:47

鼠標指針交互效果CSS

2021-04-19 10:47:11

NettyDemoI

2015-03-12 10:46:30

代碼代碼犯罪

2021-02-20 16:01:26

Github前端開發(fā)

2023-11-28 12:19:49

C++函數(shù)指針

2022-05-20 07:36:02

LiveTerm工具
點贊
收藏

51CTO技術(shù)棧公眾號