目錄
happens-before的作用" >happens-before的作用
什麼是Happens Before" >什麼是Happens Before
Go中的同步" >Go中的同步
Go的初始化" >Go的初始化
goroutine的创建结束" >goroutine的创建结束
Channel的发送和接收" >Channel的发送和接收
Lock" >Lock
Once" >Once
不正确的同步" >不正确的同步
首頁 後端開發 Golang 白話Go記憶體模型Happen-Before

白話Go記憶體模型Happen-Before

Jul 24, 2023 pm 03:47 PM
go 記憶體模型

當多個goroutine並發同時存取同一個資料時必須把並發的存取操作序列化。在Go中保證讀寫的序列化可以透過channel通訊或其他同步原語(例如sync套件中的互斥鎖、讀寫鎖和sync/atomic中的原子操作)。

Happens Before

在單一goroutine中,讀取和寫入的行為一定是和程式指定的執行順序表現一致。換言之,編譯器和處理器在不改變語言規範所定義的行為前提下才可以對單一goroutine中的指令進行重排序。

a := 1
b := 2
登入後複製

由於指令重新排序,b := 2可能先於a := 1執行。在單goroutine中,該執行順序的調整並不會影響最終結果。但多個goroutine場景下可能就會出現問題。

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

執行上述程式碼時,預期goroutine B能夠正常輸出5,但因為指令重排序,b := 1可能先於a := 5執行,最終goroutine B可能輸出0。

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

#為了明確讀取和寫入的操作的要求,Go中引入了happens before,它表示執行記憶體操作的一種偏序關係。

happens-before的作用

#多個goroutine存取共享變數時,它們必須建立同步事件來確保happens-before條件,以此確保讀取能夠觀察預期的寫入。

什麼是Happens Before

#如果事件e1發生在事件e2之前,那麼我們說e2發生在e1之後。同樣,如果e1不在e2之前發生也沒有在e2之後發生,那我們說e1和e2同時發生。

在單一goroutine中,happens-before的順序就是程式執行的順序。那happens-before到底是什麼順序呢?讓我們看看下面的條件。

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

  1. r沒有發生在w之前。
  2. 沒有其他寫入作業發生在w之後和r之前。

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

  1. w發生在r之前。
  2. 其他寫入操作發生在w之前後者r之後。

單一goroutine中不存在並發,這兩個條件是等價的。老許在此基礎上擴展一下,對於單核心的運作環境這兩組條件同樣等價。並發情況下,後一組條件比第一組更嚴格。

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

白話Go記憶體模型Happen-Before

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

白話Go記憶體模型Happen-Before

在繼續分析之前,要先感謝我的語文老師,沒有你我就無法發現它們的不同。

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

白話Go記憶體模型Happen-Before

沒有其他寫入作業發生在w之後和r之前,則其他寫w'可能發生在w之前或是和w同時發生,也可能發生在r之後或和r同時發生,如下圖(實心表示可同時)。

白話Go記憶體模型Happen-Before

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

白話Go記憶體模型Happen-Before

到這兒應該要明白為什麼第二組條件比第一組條件更嚴格了吧。在第一組的條件下是允許觀察到w,第二組是保證能觀察到w。

Go中的同步

下面是Go中約定好的一些同步事件,它們能確保程式遵循happens-before原則,從而使並發的goroutine相對有序。

Go的初始化

程序初始化运行在单个goroutine中,但是该goroutine可以创建其他并发运行的goroutine。

如果包p导入了包q,则q包init函数执行结束先于p包init函数的执行。main函数的执行发生在所有init函数执行完成之后。

goroutine的创建结束

goroutine的创建先于goroutine的执行。老许觉得这基本就是废话,但事情总是没有那么简单,其隐含之意大概是goroutine的创建是阻塞的。

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

go fmt.Println(sleep())
登入後複製

上述代码会阻塞主goroutine一秒,然后才创建子goroutine。

goroutine的退出是无法预测的。如果用一个goroutine观察另一个goroutine,请使用锁或者Channel来保证相对有序。

Channel的发送和接收

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

  • Channel的发送动作先于相应的接受动作完成之前。

  • 无缓冲Channel的接受先于该Channel上的发送完成之前。

这两点总结起来分别是开始发送开始接受发送完成接受完成四个动作,其时序关系如下。

开始发送 > 接受完成
开始接受 > 发送完成
登入後複製

注意:开始发送和开始接受并无明确的先后关系

  • Channel的关闭发生在由于通道关闭而返回零值接受之前。

  • 容量为C的Channel第k个接受先于该Channel上的第k+C个发送完成之前。

这里使用极限法应该更加易于理解,如果C为0,k为1则其含义和无缓冲Channel的一致。

Lock

对于任何sync.Mutex或sync.RWMutex变量l以及n < m,第n次l.Unlock()的调用先于第m次l.Lock()的调用返回。

假设n为1,m为2,则第二次调用l.Lock()返回前一定要先调用l.UnLock()。

对于sync.RWMutex的变量l存在这样一个n,使得l.RLock()的调用返回在第n次l.Unlock()之后发生,而与之匹配的l.RUnlock()发生在第n + 1次l.Lock()之前。

不得不说,上面这句话简直不是人能理解的。老许将其翻译成人话:

有写锁时:l.RLock()的调用返回发生在l.Unlock()之后。

有读锁时:l.RUnlock()的调用发生在l.Lock()之前。

注意:调用l.RUnlock()前不调用l.RLock()和调用l.Unlock()前不调用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()
}
登入後複製

这个例子看起来挺简单,但是老许相信大部分人应该会忽略指令重排序引起的异常输出。假如goroutine f指令重排序后,b=2先于a=1发生,此时主goroutine观察到b发生变化而未观察到a变化,因此有可能输出20

老许在本地实验了多次结果都是输出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”。说实话老许自己都不敢保证以前没有写过这样的代码。现在唯一能想到的场景就是其中一个goroutine doprint执行到done = true(指令重排序导致done=true先于a="hello, world"执行)时,另一个goroutine doprint刚开始执行并观察到done的值为true从而打印空字符串。

以上是白話Go記憶體模型Happen-Before的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它們
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

深入理解 Golang 函數生命週期與變數作用域 深入理解 Golang 函數生命週期與變數作用域 Apr 19, 2024 am 11:42 AM

在Go中,函數生命週期包括定義、載入、連結、初始化、呼叫和返回;變數作用域分為函數級和區塊級,函數內的變數在內部可見,而區塊內的變數僅在區塊內可見。

如何在 Go 中使用正規表示式匹配時間戳記? 如何在 Go 中使用正規表示式匹配時間戳記? Jun 02, 2024 am 09:00 AM

在Go中,可以使用正規表示式比對時間戳記:編譯正規表示式字串,例如用於匹配ISO8601時間戳記的表達式:^\d{4}-\d{2}-\d{2}T \d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-][0-9]{2}:[0-9]{2})$ 。使用regexp.MatchString函數檢查字串是否與正規表示式相符。

Go WebSocket 訊息如何發送? Go WebSocket 訊息如何發送? Jun 03, 2024 pm 04:53 PM

在Go中,可以使用gorilla/websocket包發送WebSocket訊息。具體步驟:建立WebSocket連線。傳送文字訊息:呼叫WriteMessage(websocket.TextMessage,[]byte("訊息"))。發送二進位訊息:呼叫WriteMessage(websocket.BinaryMessage,[]byte{1,2,3})。

Golang 與 Go 語言的區別 Golang 與 Go 語言的區別 May 31, 2024 pm 08:10 PM

Go和Go語言是不同的實體,具有不同的特性。 Go(又稱Golang)以其並發性、編譯速度快、記憶體管理和跨平台優點而聞名。 Go語言的缺點包括生態系統不如其他語言豐富、文法更嚴格、缺乏動態類型。

Golang 技術效能優化中如何避免記憶體洩漏? Golang 技術效能優化中如何避免記憶體洩漏? Jun 04, 2024 pm 12:27 PM

記憶體洩漏會導致Go程式記憶體不斷增加,可通過:關閉不再使用的資源,如檔案、網路連線和資料庫連線。使用弱引用防止記憶體洩漏,當物件不再被強引用時將其作為垃圾回收目標。利用go協程,協程棧記憶體會在退出時自動釋放,避免記憶體洩漏。

Go 並發函數的單元測試指南 Go 並發函數的單元測試指南 May 03, 2024 am 10:54 AM

對並發函數進行單元測試至關重要,因為這有助於確保其在並發環境中的正確行為。測試並發函數時必須考慮互斥、同步和隔離等基本原理。可以透過模擬、測試競爭條件和驗證結果等方法對並發函數進行單元測試。

如何使用 Golang 的錯誤包裝器? 如何使用 Golang 的錯誤包裝器? Jun 03, 2024 pm 04:08 PM

在Golang中,錯誤包裝器允許你在原始錯誤上追加上下文訊息,從而創建新錯誤。這可用於統一不同程式庫或元件拋出的錯誤類型,簡化偵錯和錯誤處理。步驟如下:使用errors.Wrap函數將原有錯誤包裝成新錯誤。新錯誤包含原始錯誤的上下文資訊。使用fmt.Printf輸出包裝後的錯誤,提供更多上下文和可操作性。在處理不同類型的錯誤時,使用errors.Wrap函數統一錯誤類型。

如何在 Go 中創建優先級 Goroutine? 如何在 Go 中創建優先級 Goroutine? Jun 04, 2024 pm 12:41 PM

在Go語言中建立優先權Goroutine有兩步驟:註冊自訂Goroutine建立函數(步驟1)並指定優先權值(步驟2)。這樣,您可以建立不同優先順序的Goroutine,優化資源分配並提高執行效率。

See all articles