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

我們一起聊聊 Go 內(nèi)存模型

開發(fā) 前端
為什么要學(xué)習(xí) Go 內(nèi)存模型? 因?yàn)檫@是理解和掌握 Go 并發(fā)編程的基礎(chǔ),也是學(xué)習(xí) Go 標(biāo)準(zhǔn)庫(kù)底層源碼實(shí)現(xiàn)的前提。

概述

為什么要學(xué)習(xí) Go 內(nèi)存模型? 因?yàn)檫@是理解和掌握 Go 并發(fā)編程的基礎(chǔ),也是學(xué)習(xí) Go 標(biāo)準(zhǔn)庫(kù)底層源碼實(shí)現(xiàn)的前提。

Go 內(nèi)存模型?指定了在什么條件下,一個(gè) goroutine? 對(duì)變量的寫操作可以被另一個(gè) goroutine 讀取到。

建議

當(dāng)一份數(shù)據(jù)同時(shí)被多個(gè) goroutine 讀取時(shí),在修改這份數(shù)據(jù)時(shí),必須序列化讀取順序。

要序列化讀取,請(qǐng)使用通道或其他同步原語(yǔ)(例如 sync 包)來保護(hù)數(shù)據(jù)。

內(nèi)存模型

內(nèi)存模型? 描述了對(duì)程序執(zhí)行的要求,這些要求由 goroutine? 執(zhí)行組成,而 goroutine 執(zhí)行又由內(nèi)存操作組成。

內(nèi)存操作由四個(gè)細(xì)節(jié)概括:

  • ? 類型: 如普通數(shù)據(jù)讀取、普通數(shù)據(jù)寫入、同步操作、原子數(shù)據(jù)讀取、互斥操作或通道操作
  • ? 代碼中的位置
  • ? 正在讀取的內(nèi)存位置
  • ? 正在讀取或?qū)懭氲闹?/li>

goroutine 的執(zhí)行過程被抽象為一組內(nèi)存操作。

同步

happens before

在一個(gè) goroutine 內(nèi)部,即使 CPU 或者編譯器進(jìn)行了指令重排,程序執(zhí)行順序依舊和代碼指定的順序一致。

對(duì)于多個(gè) goroutine 之間的通信,則需要依賴于 Go 保證的 happens before 規(guī)則約束。

下面來介紹不同場(chǎng)景下的 happens before 規(guī)則。

初始化

程序初始化在單個(gè) goroutine? 中運(yùn)行 (main goroutine?),但是 main goroutine? 可能會(huì)并發(fā)運(yùn)行其他 goroutine。

如果包 p 導(dǎo)入包 q,則 q 的 init 函數(shù)在 p 的 init 函數(shù)之前完成。

所有 init? 函數(shù)在主函數(shù) main.main 開始之前完成。

goroutine 構(gòu)造

內(nèi)存模型? 保證啟動(dòng)新 goroutine? 的 go? 語(yǔ)句會(huì)在 goroutine 內(nèi)部語(yǔ)句執(zhí)行之前完成。

如下代碼所示:

package main

var a string

func f() {
print(a)
}

func hello() {
a = "hello, world"
go f()
}

調(diào)用 hello 將在將來某一時(shí)刻打印 "hello, world"(也可能在 hello 返回之后打?。?。

goroutine 銷毀

內(nèi)存模型? 不保證 goroutine 的退出發(fā)生在程序中的任何事件之前。

如下代碼所示:

package main

var a string

func hello() {
go func() { a = "hello" }()
print(a)
}

對(duì) a 的賦值之后沒有任何同步事件,因此不能保證任何其他 goroutine? 都看到更新后的 a 的值。事實(shí)上,激進(jìn)的編譯器可能會(huì)刪除整個(gè) go func() ... 語(yǔ)句。

如果一個(gè) goroutine? 的結(jié)果必須被另一個(gè) goroutine 看到,必須使用同步機(jī)制(例如鎖或通道通信)來建立相對(duì)順序。

channel 通信

channel? 通信是 goroutine? 之間同步的主要方式,特定 channel? 上的發(fā)送和接收是一一對(duì)應(yīng)的,通常在不同的 goroutine 上進(jìn)行。

channel 的發(fā)送操作發(fā)生在對(duì)應(yīng)的接收操作完成之前 (happens before)。

如下代碼所示:

package main

var c = make(chan int, 10)
var a string

func f() {
a = "hello, world"
c <- 0
}

func main() {
go f()
<-c
print(a)
}

程序確保打印 "hello world" :

  • ? 對(duì) a 的寫操作發(fā)生在 c 的發(fā)送操作之前 (happens before)
  • ? c 的發(fā)送操作發(fā)生在 c 的接收操作完成之前 (happens before)
  • ? c 的接收操作發(fā)生在 print 之前 (happens before)

對(duì) channel? 的 close? 操作發(fā)生在 channel? 的接收操作之前 (happens before),且由于 channel 被關(guān)閉,接收方將會(huì)收到一個(gè)零值。

在前面的示例中,將 c <- 0 替換為 close(c), 程序的行為不會(huì)發(fā)生變化。

unbuffered channel 的接收操作發(fā)生在發(fā)送操作完成之前。

如下代碼所示(和上面的程序差不多,但交換了發(fā)送和接收語(yǔ)句并使用了一個(gè) unbuffered channel):

package main

var c = make(chan int)
var a string

func f() {
a = "hello, world"
<-c
}

func main() {
go f()
c <- 0
print(a)
}

這段代碼同樣能保證最終輸出 "hello, world":

  • ? 對(duì) a 的寫入發(fā)生在 c 的之前 (happens before)
  • ? c 的接收操作發(fā)生在 c 的發(fā)送操作完成之前
  • ? c 的發(fā)送操作發(fā)生在 print 操作之前

如果 channel? 為緩沖(例如,c = make(chan int, 1)),那么程序?qū)⒉荒鼙WC打印 "hello, world" (它可能會(huì)打印空字符串、崩潰或執(zhí)行其他操作)。

容量為 C 的 channel 上的第 k 個(gè)接收操作發(fā)生在第 k+C 個(gè)發(fā)送操作之前。

這條規(guī)則可以視為對(duì)上面規(guī)則的拓展,(當(dāng) c = 0 時(shí)就是一個(gè) unbuffered channel? 了),可以使用 buffered channel? 封裝出一個(gè)信號(hào)量 (semaphore), 用 channel? 里面的元素?cái)?shù)量來代表當(dāng)前正在使用的資源數(shù)量,channel? 的容量表示同時(shí)可以使用的最大資源數(shù)量。當(dāng)申請(qǐng)信號(hào)量時(shí),就往 channel? 中發(fā)送一個(gè)元素, 釋放信號(hào)量時(shí)就從 channel 中接收一個(gè)元素,這是限制并發(fā)的常用操作。

下面的程序?yàn)?nbsp;work? 列表中的每個(gè)元素啟動(dòng)一個(gè) goroutine?,并使用名字 limit 的 channel 來協(xié)調(diào)協(xié)程,保證同一時(shí)刻最多有三個(gè)方法在執(zhí)行 。

package main

var limit = make(chan int, 3)

func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{}
}

sync? 包實(shí)現(xiàn)了兩種鎖數(shù)據(jù)類型,sync.Mutex? 和 sync.RWMutex。

**對(duì)于任何 sync.Mutex? 或 sync.RWMutex? 變量 l,在 n < m 的條件下,對(duì) l.Unlock()? 的第 n 次調(diào)用發(fā)生在 l.Lock() 的第 m 次調(diào)用的返回之前 (happens before)**。

如下代碼所示:

package main

var l sync.Mutex
var a string

func f() {
a = "hello, world"
l.Unlock()
}

func main() {
l.Lock()
go f()
l.Lock()
print(a)
}

上面的代碼保證會(huì)輸出 "hello, world":

  • ? l.Unlock()? 的第一次調(diào)用 (在 f() 內(nèi)) 發(fā)生在第二次調(diào)用 l.lock() 返回之前 (在 main) (happens before)
  • ? 第二次調(diào)用 l.lock() 發(fā)生在 print(a) 之前 (happens before)

對(duì)于類型為 sync.RWMutex? 的變量 l,對(duì)任何一次 l.RLock()? 的調(diào)用,都會(huì)存在一個(gè) n,使得 l.RLock()? 發(fā)生在第 n 次調(diào)用 l.Unlock()? 之后, 并發(fā)生在第 n + 1 次 l.Lock 之前。

sync.Once

sync? 包通過使用 Once? 類型在存在多個(gè) goroutine 的情況下提供了一種安全的初始化機(jī)制。多個(gè)線程可以為特定的 f 執(zhí)行一次 Do(f), 但只有一個(gè)會(huì)運(yùn)行 f(),而其他調(diào)用將阻塞直到 f() 返回。

once.Do(f)? 中 f() 將會(huì)在所有的 once.Do(f) 返回之前返回 (happens before)。

如下代碼所示:

package main

var a string
var once sync.Once

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

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

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

twoprint? 只會(huì)調(diào)用一次 setup?。 setup 將在調(diào)用 print 之前完成,結(jié)果是 "hello, world" 被打印兩次。

Atomic Values

sync/atomic? 包中的 API 統(tǒng)稱為 "原子操作",可用于同步不同 goroutine 的執(zhí)行。如果原子操作 A 的效果被原子操作 B 讀取到, 那么 A 在 B 之前同步,程序中執(zhí)行的所有原子操作的行為就像順序執(zhí)行一樣。

Finalizers

runtime? 包提供了一個(gè) SetFinalizer? 函數(shù),該函數(shù)添加了一個(gè)終結(jié)器,當(dāng)程序不再可以讀取特定對(duì)象時(shí)將調(diào)用該終結(jié)器,在完成調(diào)用 f(x) 之前同步調(diào)用 SetFinalizer(x, f)。

其他同步原語(yǔ)

sync? 包還提供了其他同步原語(yǔ),包括 sync.Cond?, sync.Map?, sync.Pool?, sync.WaitGroup。

錯(cuò)誤的同步

存在競(jìng)態(tài)的程序是不正確的,并且可以表現(xiàn)出非順序一致的執(zhí)行。

如下代碼所示:

package main

var a, b int

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

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

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

g() 可能會(huì)發(fā)生先輸出 2 再輸出 0 的情況。

雙重檢查鎖定是一種避免同步開銷的嘗試,如下代碼所示,twoprint 程序可能被錯(cuò)誤地寫成:

package main

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()
}

在 doprint 內(nèi),即使讀取到了 done 變量被更新為 true,也并不能保證 a 變量被更新為 "hello, world" 了。因此上面的程序可能會(huì)打印出一個(gè)空字符串。

下面是一段忙等待的代碼,它的原本目的是:無限循環(huán),直至變量 a 被賦值。

package main

var a string
var done bool

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

func main() {
go setup()
for !done {
}
print(a)
}

和上面一樣,讀取到 done 的寫操作并不能表示能讀取到對(duì) a 的寫操作,所以這段代碼也可能會(huì)打印出一個(gè)空白的字符串。更糟的是, 由于不能保證 done 的寫操作一定會(huì)被 main 讀取到,main 可能會(huì)進(jìn)入無限循環(huán)。

如下代碼所示:

package main

type T struct {
msg string
}

var g *T

func setup() {
t := new(T)
t.msg = "hello, world"
g = t
}

func main() {
go setup()
for g == nil {
}
print(g.msg)
}

即使 main 讀取到 g != nil 并退出循環(huán),也不能保證它會(huì)讀取到 g.msg 的初始化值。

在剛才所有這些示例代碼中,問題的解決方案都是相同的:使用顯式同步。

錯(cuò)誤的編譯

Go 內(nèi)存模型 限制編譯器優(yōu)化,就像限制 Go 程序一樣,一些在單線程程序中有效的編譯器優(yōu)化并非在所有 Go 程序中都有效。 尤其是編譯器不得引入原始程序中不存在的寫入操作,不得允許單個(gè)操作讀取多個(gè)值,并且不得允許單次操作寫入多個(gè)值。

以下所有示例均假定 "*p" 和 "*q" 指的是多個(gè) goroutine 都可讀取的指針變量。

不將數(shù)據(jù)競(jìng)態(tài)引入無競(jìng)態(tài)程序,意味著不將寫入操作從它們出現(xiàn)的條件語(yǔ)句中移出。如下代碼所示,編譯器不得反轉(zhuǎn)此程序中的條件:

*p = 1
if cond {
*p = 2
}

也就是說,編譯器不得將程序重寫為以下代碼:

*p = 2
if !cond {
*p = 1
}

如果 cond? 為 false,另一個(gè) goroutine? 正在讀取 *p,那么在原來的程序中,另一個(gè) goroutine? 只能讀取到 *p? 等于 1。 在改寫后的程序中,另一個(gè) goroutine 可以讀取到 2,這在以前是不可能的。

不引入數(shù)據(jù)競(jìng)態(tài)也意味著假設(shè)循環(huán)不會(huì)終止,如下代碼所示,編譯器通常不得在該程序的循環(huán)之前移動(dòng)對(duì) *p? 或 *q 的讀取順序:

n := 0
for e := list; e != nil; e = e.next {
n++
}
i := *p
*q = 1

如果 list 指向環(huán)形鏈表,那么原始程序?qū)⒂肋h(yuǎn)不會(huì)讀取 *p? 或 *q,但重寫后的代碼可以讀取到。

不引入數(shù)據(jù)競(jìng)態(tài)也意味著,假設(shè)被調(diào)用的函數(shù)可能不返回或沒有同步操作,如下代碼所示,編譯器不得在該程序中的函數(shù)調(diào)用之前移動(dòng)對(duì) *p? 或 *q 的讀取。

f()
i := *p
*q = 1

如果調(diào)用永遠(yuǎn)不會(huì)返回,那么原始程序?qū)⒂肋h(yuǎn)讀取不到 *p? 或 *q,但重寫后的代碼可以讀取到。

不允許單次讀取多個(gè)值,意味著不從共享內(nèi)存中重新加載局部變量。如下代碼所示,編譯器不得丟棄變量 i 并再次從 *p 重新加載。

i := *p
if i < 0 || i >= len(funcs) {
panic("invalid function index")
}
... complex code ...
// compiler must NOT reload i = *p here
funcs[i]()

如果復(fù)雜代碼需要很多寄存器,單線程程序的編譯器可以丟棄變量 i 而不保存副本,然后在 funcs[i]()? 調(diào)用之前重新加載 i = *p。

不允許單次寫入多個(gè)值,意味著在寫入之前不使用局部變量的內(nèi)存作為臨時(shí)變量,如下代碼所示,編譯器不得在此程序中使用 *p 作為臨時(shí)變量:

*p = i + *p/2

也就是說,它不能將程序改寫成這個(gè):

*p /= 2
*p += i

如果 i 和 *p? 開始等于 2,則原始代碼確實(shí) *p? = 3,因此一個(gè)執(zhí)行較快線程只能從 *p? 中讀取 2 或 3。 重寫的代碼執(zhí)行 *p? = 1,然后 *p = 3,從而允許競(jìng)態(tài)線程也讀取 1。

請(qǐng)注意,所有這些優(yōu)化在 C/C++ 編譯器中都是允許的:與 C/C++ 編譯器共享后端的 Go 編譯器必須注意禁用對(duì) Go 無效的優(yōu)化。 如果編譯器可以證明數(shù)據(jù)競(jìng)態(tài)不會(huì)影響目標(biāo)平臺(tái)上的正確執(zhí)行,則不需要禁止引入數(shù)據(jù)競(jìng)態(tài)。如下代碼所示,在大多數(shù) CPU 上重寫都是有效的。

n := 0
for i := 0; i < m; i++ {
n += *shared
}

重寫為

n := 0
local := *shared
for i := 0; i < m; i++ {
n += local
}

前提是可以證明 *shared 不會(huì)出現(xiàn)讀取出錯(cuò),因?yàn)闈撛诘淖x取不會(huì)影響任何現(xiàn)有的并發(fā)讀取或?qū)懭搿?/p>

結(jié)論

當(dāng)談到有競(jìng)態(tài)的程序時(shí),程序員和編譯器都應(yīng)該記住這個(gè)建議:不要自作聰明。

筆者寄語(yǔ)

本文翻譯自官方 博客原文[1], 希望讀者在讀完本文后,能夠深入理解 happens before 在各場(chǎng)景下的規(guī)則,寫出更加健壯的并發(fā)程序。

Reference

  • ? The Go Memory Model[2]
  • ? Memory Reordering[3]
  • ? Updating the Go Memory Model[4]

引用鏈接

[1]? 博客原文: https://go.dev/ref/mem[2]? The Go Memory Model: https://go.dev/ref/mem[3]? Memory Reordering: https://cch123.github.io/ooo/[4]? Updating the Go Memory Model: https://research.swtch.com/gomm

責(zé)任編輯:武曉燕 來源: 洋芋編程
相關(guān)推薦

2024-02-26 00:00:00

Go性能工具

2022-07-29 08:17:46

Java對(duì)象內(nèi)存

2024-05-20 11:33:20

AI模型數(shù)據(jù)

2024-03-11 00:09:00

模型融合場(chǎng)景

2024-02-19 10:11:00

Kubernetes網(wǎng)絡(luò)模型

2021-08-27 07:06:10

IOJava抽象

2024-02-20 21:34:16

循環(huán)GolangGo

2023-08-04 08:20:56

DockerfileDocker工具

2022-05-24 08:21:16

數(shù)據(jù)安全API

2023-08-10 08:28:46

網(wǎng)絡(luò)編程通信

2023-09-10 21:42:31

2023-06-30 08:18:51

敏捷開發(fā)模式

2024-09-05 10:36:58

2024-06-27 08:54:22

Go模塊團(tuán)隊(duì)

2024-05-17 08:47:33

數(shù)組切片元素

2022-02-14 07:03:31

網(wǎng)站安全MFA

2022-06-26 09:40:55

Django框架服務(wù)

2022-01-04 12:08:46

設(shè)計(jì)接口

2022-10-28 07:27:17

Netty異步Future

2023-04-26 07:30:00

promptUI非結(jié)構(gòu)化
點(diǎn)贊
收藏

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