<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          白話Go內(nèi)存模型&Happen-Before

          共 4148字,需瀏覽 9分鐘

           ·

          2022-04-07 17:44

          點擊上方“Go語言進階學(xué)習(xí)”,進行關(guān)注

          回復(fù)“Go語言”即可獲贈Python從入門到進階共10本電子書

          晚年唯好靜,萬事不關(guān)心。

          Go內(nèi)存模型明確指出,一個goroutine如何才能觀察到其他goroutine對同一變量的寫操作。

          當(dāng)多個goroutine并發(fā)同時存取同一個數(shù)據(jù)時必須把并發(fā)的存取操作序列化。在Go中保證讀寫的序列化可以通過channel通信或者其他同步原語(例如sync包中的互斥鎖、讀寫鎖和sync/atomic中的原子操作)。

          Happens Before

          在單goroutine中,讀取和寫入的行為一定是和程序指定的執(zhí)行順序表現(xiàn)一致。換言之,編譯器和處理器在不改變語言規(guī)范所定義的行為前提下才可以對單個goroutine中的指令進行重排序。

          a := 1
          b := 2

          由于指令重排序,b := 2可能先于a := 1執(zhí)行。單goroutine中,該執(zhí)行順序的調(diào)整并不會影響最終結(jié)果。但多個goroutine場景下可能就會出現(xiàn)問題。

          var a, b int
          // goroutine A
          go func() {
              a := 5
              b := 1
          }()
          // goroutine B
          go func() {
              for b == 1 {}
              fmt.Println(a)
          }()

          執(zhí)行上述代碼時,預(yù)期goroutine B能夠正常輸出5,但因為指令重排序,b := 1可能先于a := 5執(zhí)行,最終goroutine B可能輸出0。

          :上述例子是個不正確的示例,僅作說明用。

          為了明確讀寫的操作的要求,Go中引入了happens before,它表示執(zhí)行內(nèi)存操作的一種偏序關(guān)系。

          happens-before的作用

          多個goroutine訪問共享變量時,它們必須建立同步事件來確保happens-before條件,以此確保讀能夠觀察預(yù)期的寫。

          什么是Happens Before

          如果事件e1發(fā)生在事件e2之前,那么我們說e2發(fā)生在e1之后。同樣,如果e1不在e2之前發(fā)生也沒有在e2之后發(fā)生,那么我們說e1和e2同時發(fā)生。

          在單個goroutine中,happens-before的順序就是程序執(zhí)行的順序。那happens-before到底是什么順序呢?我們看看下面的條件。

          如果對于一個變量v的讀操作r和寫操作w滿足下述兩個條件,r才允許觀察到w:

          1. r沒有發(fā)生在w之前。
          2. 沒有其他寫操作發(fā)生在w之后和r之前。

          為了保證變量v的一個讀操作r能夠觀察到一個特定的寫操作w,需要確保w是唯一允許被r觀察的寫操作。那么,如果 r、w 都滿足以下條件,r就能確保觀察到w:

          1. w發(fā)生在r之前。
          2. 其他寫操作發(fā)生在w之前后者r之后。

          單goroutine中不存在并發(fā),這兩個條件是等價的。老許在此基礎(chǔ)上擴展一下,對于單核心的運行環(huán)境這兩組條件同樣等價。并發(fā)情況下,后一組條件比第一組更加嚴格。

          假如你很疑惑,那就對了!老許最開始也很疑惑,這兩組條件就是一樣的呀。為此老許特地和原文進行了反復(fù)對比確保上述的理解是沒有問題的。

          我們換個思路,進行反向推理。如果這兩組條件一樣,那原文沒必要寫兩次,果然此事并不簡單。

          在繼續(xù)分析之前,要先感謝一下我的語文老師,沒有你我就無法發(fā)現(xiàn)它們的不同。

          r沒有發(fā)生在w之前,則r可能的情況是r發(fā)生在w之后或者和w同時發(fā)生,如下圖(實心表示可同時)。

          沒有其他寫操作發(fā)生在w之后和r之前,則其他寫w'可能發(fā)生在w之前或者和w同時發(fā)生,也可能發(fā)生在r之后或者和r同時發(fā)生,如下圖(實心表示可同時)。

          第二組條件就很明確了,w發(fā)生在r之前且其他寫操作只能發(fā)生在w之前或者r之后,如下圖(空心表示不可同時)。

          到這兒應(yīng)該明白為什么第二組條件比第一組條件更加嚴格了吧。在第一組的條件下是允許觀察到w,第二組是保證能觀察到w。

          Go中的同步

          下面是Go中約定好的一些同步事件,它們能確保程序遵循h(huán)appens-before原則,從而使并發(fā)的goroutine相對有序。

          Go的初始化

          程序初始化運行在單個goroutine中,但是該goroutine可以創(chuàng)建其他并發(fā)運行的goroutine。

          如果包p導(dǎo)入了包q,則q包init函數(shù)執(zhí)行結(jié)束先于p包init函數(shù)的執(zhí)行。main函數(shù)的執(zhí)行發(fā)生在所有init函數(shù)執(zhí)行完成之后。

          goroutine的創(chuàng)建結(jié)束

          goroutine的創(chuàng)建先于goroutine的執(zhí)行。老許覺得這基本就是廢話,但事情總是沒有那么簡單,其隱含之意大概是goroutine的創(chuàng)建是阻塞的。

          func sleep() bool {
             time.Sleep(time.Second)
             return true
          }

          go fmt.Println(sleep())

          上述代碼會阻塞主goroutine一秒,然后才創(chuàng)建子goroutine。

          goroutine的退出是無法預(yù)測的。如果用一個goroutine觀察另一個goroutine,請使用鎖或者Channel來保證相對有序。

          Channel的發(fā)送和接收

          Channel通信是goroutine之間同步的主要方式。

          • Channel的發(fā)送動作先于相應(yīng)的接受動作完成之前。

          • 無緩沖Channel的接受先于該Channel上的發(fā)送完成之前。

          這兩點總結(jié)起來分別是開始發(fā)送開始接受、發(fā)送完成接受完成四個動作,其時序關(guān)系如下。

          開始發(fā)送 > 接受完成
          開始接受 > 發(fā)送完成

          注意:開始發(fā)送和開始接受并無明確的先后關(guān)系

          • Channel的關(guān)閉發(fā)生在由于通道關(guān)閉而返回零值接受之前。

          • 容量為C的Channel第k個接受先于該Channel上的第k+C個發(fā)送完成之前。

          這里使用極限法應(yīng)該更加易于理解,如果C為0,k為1則其含義和無緩沖Channel的一致。

          Lock

          對于任何sync.Mutex或sync.RWMutex變量l以及n < m,第n次l.Unlock()的調(diào)用先于第m次l.Lock()的調(diào)用返回。

          假設(shè)n為1,m為2,則第二次調(diào)用l.Lock()返回前一定要先調(diào)用l.UnLock()。

          對于sync.RWMutex的變量l存在這樣一個n,使得l.RLock()的調(diào)用返回在第n次l.Unlock()之后發(fā)生,而與之匹配的l.RUnlock()發(fā)生在第n + 1次l.Lock()之前。

          不得不說,上面這句話簡直不是人能理解的。老許將其翻譯成人話:

          有寫鎖時:l.RLock()的調(diào)用返回發(fā)生在l.Unlock()之后。

          有讀鎖時:l.RUnlock()的調(diào)用發(fā)生在l.Lock()之前。

          注意:調(diào)用l.RUnlock()前不調(diào)用l.RLock()和調(diào)用l.Unlock()前不調(diào)用l.Lock()會引起panic。

          Once

          once.Do(f)中f的返回先于任意其他once.Do的返回。

          不正確的同步

          錯誤示范一

          var a, b int

          func f() {
           a = 1
           b = 2
          }

          func g() {
           print(b)
           print(a)
          }

          func main() {
           go f()
           g()
          }

          這個例子看起來挺簡單,但是老許相信大部分人應(yīng)該會忽略指令重排序引起的異常輸出。假如goroutine f指令重排序后,b=2先于a=1發(fā)生,此時主goroutine觀察到b發(fā)生變化而未觀察到a變化,因此有可能輸出20。

          老許在本地實驗了多次結(jié)果都是輸出0020這個輸出估計只活在理論之中了。

          錯誤示范二

          var a string
          var done bool

          func setup() {
           a = "hello, world"
           done = true
          }

          func doprint() {
           if !done {
            once.Do(setup)
           }
           print(a)
          }

          func twoprint() {
           go doprint()
           go doprint()
          }

          這種雙重檢測本意是為了避免同步的開銷,但是依舊有可能打印出空字符串而不是“hello, world”。說實話老許自己都不敢保證以前沒有寫過這樣的代碼?,F(xiàn)在唯一能想到的場景就是其中一個goroutine doprint執(zhí)行到done = true(指令重排序?qū)е?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(255, 93, 108);">done=true先于a="hello, world"執(zhí)行)時,另一個goroutine doprint剛開始執(zhí)行并觀察到done的值為true從而打印空字符串。

          最后,衷心希望本文能夠?qū)Ω魑蛔x者有一定的幫助。當(dāng)然,發(fā)現(xiàn)錯誤也還請及時聯(lián)系老許改正。

          ------------------- End -------------------

          往期精彩文章推薦:

          歡迎大家點贊,轉(zhuǎn)發(fā),轉(zhuǎn)載,感謝大家的相伴與支持

          想加入學(xué)習(xí)群請在后臺回復(fù)【入群

          萬水千山總是情,點個【在看】行不行

          瀏覽 36
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  乱伦一区二区三区 | 国产精品视频福利 | 丁香色五月婷婷 | 国产福利AV | 午夜在线 |