電子產(chǎn)業(yè)一站式賦能平臺

PCB聯(lián)盟網(wǎng)

搜索
查看: 34|回復: 0
收起左側(cè)

面試官不講武德,偷襲我一個大學生:當"我知道答案"遇上"請解釋原理"

[復制鏈接]

285

主題

285

帖子

2558

積分

三級會員

Rank: 3Rank: 3

積分
2558
跳轉(zhuǎn)到指定樓層
樓主
發(fā)表于 3 天前 | 只看該作者 |只看大圖 回帖獎勵 |倒序瀏覽 |閱讀模式
最近有讀者和我描述他最近的一次面試,他說面試官不講武德,我問發(fā)生了啥,原來是這么回事:
讀者(我們簡稱為A)去面試,開始問了一些C++多態(tài)方面的,回答一切順利,后面就情況不對了。

開始


面試官:static局部變量是線程安全的嗎?

A(竊喜 這題簡單啊):C++11之前不安全,C++11之后是安全的

面試官:好,那你知道原理嗎?C++11后是如何實現(xiàn)保證線程安全的嗎?

A:此處愣神30秒,(內(nèi)心:我看的八股文也沒講原理。

面試官:可以從GCC、CLang、MSVC 或者其他常用編譯器說一個熟悉的就行

A:我尼瑪,再次愣神30秒,回答:應該是加了鎖吧

面試官:好,那具體是怎么加的?只有一個鎖就能實現(xiàn)效果嗎?

A:(內(nèi)心:這面試官不講武德啊,我不知道。┗卮穑何也皇呛芏,我就是知道它線程安全,沒有研究過原理。

面試官:嗯哼

A:內(nèi)心:嗯哼你妹啊

面試官:
C++無鎖隊列的底層原理是什么?
紅黑樹原理是什么?
多路復用select原理是什么?
......

A內(nèi)心直呼:這面試官不講武德啊,為難我一個沒工作的大學生
面試結(jié)束

嗯,好吧,面試官確實有點不講武德,不過現(xiàn)在大環(huán)境這么卷,沒得辦法。
我們就來拆解下:C++11如何保證static局部變量初始化的線程安全?
首先強調(diào)一點,C++11保證的只是初始化的安全。
首先,我要推薦一個網(wǎng)站,可以實時的把代碼轉(zhuǎn)換為編譯器里面的代碼
  • https://compiler-explorer.com/我們看個例子,左邊代碼,右邊直接顯示匯編代碼,非常方便。如果匯編代碼看不懂,現(xiàn)在可以丟給AI。


    我們把匯編代碼丟給Cursor看看,Cursor回答:
    這是一段 x86_64 匯編代碼,讓我用中文為您詳細解釋每一行的含義:
  • func(int):                      # 函數(shù)聲明,接受一個整數(shù)參數(shù)        push    rbp             # 保存舊的基址指針        mov     rbp, rsp        # 設(shè)置新的基址指針,建立新的棧幀        mov     DWORD PTR [rbp-4], edi   # 將參數(shù)(存儲在edi寄存器中)存入局部變量        add     DWORD PTR [rbp-4], 20    # 將局部變量的值加20        mov     eax, DWORD PTR [rbp-4]   # 將結(jié)果移動到eax寄存器(作為返回值)        pop     rbp             # 恢復舊的基址指針        ret                     # 返回函數(shù)這個函數(shù)的功能很簡單:
    接收一個整數(shù)參數(shù)
    將這個參數(shù)加上20
    返回計算結(jié)果
    用C語言表示相當于:
  • int func(int x) {    return x + 20;}
    不得不說AI工具確實強大,直接反推出來了函數(shù)。好了,我們言歸正傳,看一下靜態(tài)局部變量:
    寫如下代碼:


    右邊匯編代碼如下:

    看這個匯編代碼,看不出來哪里對初始化線程安全有做處理,上面是GCC,我們換成CLang再試一次

    換成CLang,發(fā)現(xiàn)多了一個global_var_init,但是這個和static沒關(guān)系,這個是會導致生成的。
    可以發(fā)現(xiàn),對于int類型,不好觀察。因為int類型太簡單了,編譯器可能執(zhí)行了優(yōu)化處理。下一步我們把int類型換成string類型再看看。

    匯編代碼,有點長不好截圖
  • .LC0:        .string "Hello"func[abi:cxx11]():        push    rbp        mov     rbp, rsp        push    r12        push    rbx        sub     rsp, 32        mov     QWORD PTR [rbp-40], rdi        movzx   eax, BYTE PTR guard variable for func[abi:cxx11]()::key[rip]        test    al, al        sete    al        test    al, al        je      .L6        mov     edi, OFFSET FLAT:guard variable for func[abi:cxx11]()::key        call    __cxa_guard_acquire        test    eax, eax        setne   al        test    al, al        je      .L6        mov     r12d, 0        lea     rax, [rbp-25]        mov     QWORD PTR [rbp-24], rax        nop        nop        lea     rax, [rbp-25]        mov     rdx, rax        mov     esi, OFFSET FLAT:.LC0        mov     edi, OFFSET FLAT:func[abi:cxx11]()::key        call    std::__cxx11::basic_stringchar, std::char_traitschar>, std::allocatorchar> >::basic_stringstd::allocatorchar> >(char const*, std::allocatorchar> const&)        mov     edx, OFFSET FLAT:__dso_handle        mov     esi, OFFSET FLAT:func[abi:cxx11]()::key        mov     edi, OFFSET FLAT:std::__cxx11::basic_stringchar, std::char_traitschar>, std::allocatorchar> >::~basic_string() [complete object destructor]        call    __cxa_atexit        mov     edi, OFFSET FLAT:guard variable for func[abi:cxx11]()::key        call    __cxa_guard_release        lea     rax, [rbp-25]        mov     rdi, rax        call    std::__new_allocatorchar>::~__new_allocator() [base object destructor]        nop.L6:        mov     rax, QWORD PTR [rbp-40]        mov     esi, OFFSET FLAT:func[abi:cxx11]()::key        mov     rdi, rax        call    std::__cxx11::basic_stringchar, std::char_traitschar>, std::allocatorchar> >::basic_string(std::__cxx11::basic_stringchar, std::char_traitschar>, std::allocatorchar> > const&) [complete object constructor]        jmp     .L11        mov     rbx, rax        lea     rax, [rbp-25]        mov     rdi, rax        call    std::__new_allocatorchar>::~__new_allocator() [base object destructor]        nop        test    r12b, r12b        jne     .L9        mov     edi, OFFSET FLAT:guard variable for func[abi:cxx11]()::key        call    __cxa_guard_abort.L9:        mov     rax, rbx        mov     rdi, rax        call    _Unwind_Resume.L11:        mov     rax, QWORD PTR [rbp-40]        add     rsp, 32        pop     rbx        pop     r12        pop     rbp        ret下面拆解下這份匯編代碼,看看能不能窺探到static局部變量的初始化是如何做到線程安全的?
    這段匯編代碼展示了C++中靜態(tài)局部變量的線程安全初始化機制(也稱為"Double-Checked Locking Pattern")
    首先看第一次檢查:
  • movzx   eax, BYTE PTR guard variable for func[abi:cxx11]()::key[rip]        test    al, al        sete    al        test    al, al        je      .L6這是在檢查guard變量,guard variable是一個字節(jié)大小的變量,初始值為0,如果已經(jīng)初始化則跳轉(zhuǎn)到 .L6
    第二次檢查:
  • mov     edi, OFFSET FLAT:guard variable for func[abi:cxx11]()::keycall    __cxa_guard_acquiretest    eax, eaxsetne   altest    al, alje      .L6第二次檢查是在獲取鎖,如果guard變量為0,調(diào)用__cxa_guard_acquire,__cxa_guard_acquire嘗試獲取鎖,如果獲取失敗則轉(zhuǎn)到.L6
    __cxa_guard_acquire的實現(xiàn)通常包含:
  • 使用原子操作檢查guard變量如果已初始化,返回0如果未初始化,獲取互斥鎖雙重檢查(double-check)guard變量返回1表示獲得了初始化權(quán)限只有一個線程能獲得初始化權(quán)限,這個線程會:
  • 初始化靜態(tài)變量調(diào)用__cxa_guard_release設(shè)置guard變量釋放互斥鎖其他線程在__cxa_guard_acquire中等待,直到初始化完成

    如果初始化過程發(fā)生異常,調(diào)用__cxa_guard_abort
  • mov     edi, OFFSET FLAT:guard variable for func[abi:cxx11]()::keycall    __cxa_guard_abort
    這里實現(xiàn)線程安全的關(guān)鍵機制是:

    雙重檢查
    第一次檢查:快速路徑,檢查guard變量是否已初始化
    第二次檢查:通過__cxa_guard_acquire進行加鎖檢查
    Guard變量
    編譯器為每個靜態(tài)局部變量生成一個guard變量
    用于追蹤變量的初始化狀態(tài)
    關(guān)鍵函數(shù)
    __cxa_guard_acquire:獲取鎖,返回值表示是否需要初始化
    __cxa_guard_release:釋放鎖,標記初始化完成
    __cxa_guard_abort:初始化失敗時的清理
    到這里,應該已經(jīng)明白了面試官要問的底層原理了。


    上面我們看的是匯編代碼,下面我們再從源碼看一看,上面匯編看了GCC的,我們源碼就看CLang的。
    關(guān)于static局部變量初始化線程安全最主要的實現(xiàn)在:
  • https://github.com/llvm/llvm-project/blob/main/libcxxabi/src/cxa_guard.cpphttps://github.com/llvm/llvm-project/blob/main/libcxxabi/src/cxa_guard_impl.h源碼中詳細解釋了guard變量的定義和布局
  • The first "guard byte" (which is checked by the compiler) is set only upon * the completion of cxa release. * * The second "init byte" does the rest of the bookkeeping. It tracks if * initialization is complete or pending, and if there are waiting threads. * * If the guard variable is 64-bits and the platforms supplies a 32-bit thread * identifier, it is used to detect recursive initialization. The thread ID of * the thread currently performing initialization is stored in the second word. * *  Guard Object layout: * --------------------------------------------------------------------------- * | a+0: guard byte | a+1: init byte | a+2: unused ... | a+4: thread-id ... | * ---------------------------------------------------------------------------
    guard對象的內(nèi)存布局包含:
    guard byte (位于偏移量0): 由編譯器檢查,只在cxa release完成時設(shè)置
    init byte (位于偏移量1): 用于跟蹤初始化狀態(tài)(完成/pending)和等待線程
    unused bytes (位于偏移量2-3): 未使用的字節(jié)
    thread-id (位于偏移量4開始): 存儲當前執(zhí)行初始化的線程ID(在64位guard變量且平臺提供32位線程ID的情況下使用)
    我們看一下幾個核心類:
    GuardByte類
  • struct GuardByte {    GuardByte(uint8_t* const guard_byte_address) : guard_byte(guard_byte_address) {}        bool acquire() {        // 如果guard_byte非0,說明初始化已完成        return guard_byte.load(std::_AO_Acquire) != UNSET;    }        void release() {         guard_byte.store(COMPLETE_BIT, std::_AO_Release);     }        void abort() {} // 終止時不需要做任何事
    private:    AtomicIntuint8_t> guard_byte;};表示guard的不同狀態(tài)定義:
  • static constexpr uint8_t UNSET = 0;static constexpr uint8_t COMPLETE_BIT = (1 0);static constexpr uint8_t PENDING_BIT = (1 1);static constexpr uint8_t WAITING_BIT = (1 2);工具類 LazyValue
  • templateclass T, T (*Init)()>struct LazyValue {    LazyValue() : is_init(false) {}    T& get() {        if (!is_init) {            value = Init();            is_init = true;        }        return value;    }private:    T value;    bool is_init = false;};LazyValue是一個實現(xiàn)延遲初始化(lazy initialization)的模板工具類。
    主要工作流程:
    1、初始化檢查:
    當遇到靜態(tài)局部變量時,編譯器會生成檢查代碼
    使用guard byte來追蹤變量是否已經(jīng)初始化
    2、線程同步:
    init byte用于處理多線程情況
    可能的狀態(tài):
    UNSET:未初始化
    COMPLETE:已完成初始化
    PENDING:正在初始化
    WAITING:有線程等待初始化完成
    3、防止遞歸初始化:
    使用thread ID來檢測是否存在遞歸初始化
    64位guard變量可以存儲32位thread ID
    4、原子操作:
    使用AtomicInt類來確保線程安全
    實現(xiàn)了load、store、exchange等原子操作
    可以看到CLang源碼分析實現(xiàn)基本和GCC匯編代碼分析是一樣的,需要一個守護guard變量加上acquire鎖。


    我簡單做一個總結(jié),用一句話回答這個問題就是:
    C++11會通過編譯器生成的guard變量和調(diào)用__cxa_guard_acquire/__cxa_guard_release實現(xiàn)雙重檢查鎖模式(DCLP),確保static局部變量只被初始化一次,其他線程在初始化未完成時會在__cxa_guard_acquire內(nèi)部等待。
    更細節(jié)的部分,各位讀者可以去看源碼,這篇文章只是拋磚引玉,不過感嘆下現(xiàn)在校招太卷了,我知道答案,但是面試官反手問一個原理是啥?IT開發(fā)也不再是會用工具會調(diào)包就能獲得的崗位了。
    end

    一口Linux

    關(guān)注,回復【1024】海量Linux資料贈送
    精彩文章合集
    文章推薦
    ?【專輯】ARM?【專輯】粉絲問答?【專輯】所有原創(chuàng)?【專輯】linux入門?【專輯】計算機網(wǎng)絡(luò)?【專輯】Linux驅(qū)動?【干貨】嵌入式驅(qū)動工程師學習路線?【干貨】Linux嵌入式所有知識點-思維導圖
  • 回復

    使用道具 舉報

    發(fā)表回復

    您需要登錄后才可以回帖 登錄 | 立即注冊

    本版積分規(guī)則


    聯(lián)系客服 關(guān)注微信 下載APP 返回頂部 返回列表