偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

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

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

概述

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

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

建議

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

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

內(nèi)存模型

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

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

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

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

同步

happens before

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

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

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

初始化

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

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

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

goroutine 構(gòu)造

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

如下代碼所示:

package main

var a string

func f() {
print(a)
}

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

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

goroutine 銷毀

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

如下代碼所示:

package main

var a string

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

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

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

channel 通信

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

channel 的發(fā)送操作發(fā)生在對應(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" :

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

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

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

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

如下代碼所示(和上面的程序差不多,但交換了發(fā)送和接收語句并使用了一個 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":

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

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

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

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

下面的程序為 work? 列表中的每個元素啟動一個 goroutine?,并使用名字 limit 的 channel 來協(xié)調(diào)協(xié)程,保證同一時刻最多有三個方法在執(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? 包實現(xiàn)了兩種鎖數(shù)據(jù)類型,sync.Mutex? 和 sync.RWMutex。

**對于任何 sync.Mutex? 或 sync.RWMutex? 變量 l,在 n < m 的條件下,對 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)
}

上面的代碼保證會輸出 "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)

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

sync.Once

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

once.Do(f)? 中 f() 將會在所有的 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? 只會調(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? 包提供了一個 SetFinalizer? 函數(shù),該函數(shù)添加了一個終結(jié)器,當(dāng)程序不再可以讀取特定對象時將調(diào)用該終結(jié)器,在完成調(diào)用 f(x) 之前同步調(diào)用 SetFinalizer(x, f)。

其他同步原語

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

錯誤的同步

存在競態(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() 可能會發(fā)生先輸出 2 再輸出 0 的情況。

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

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á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 的寫操作并不能表示能讀取到對 a 的寫操作,所以這段代碼也可能會打印出一個空白的字符串。更糟的是, 由于不能保證 done 的寫操作一定會被 main 讀取到,main 可能會進入無限循環(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),也不能保證它會讀取到 g.msg 的初始化值。

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

錯誤的編譯

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

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

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

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

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

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

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

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

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

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

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

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

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

不允許單次讀取多個值,意味著不從共享內(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。

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

*p = i + *p/2

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

*p /= 2
*p += i

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

請注意,所有這些優(yōu)化在 C/C++ 編譯器中都是允許的:與 C/C++ 編譯器共享后端的 Go 編譯器必須注意禁用對 Go 無效的優(yōu)化。 如果編譯器可以證明數(shù)據(jù)競態(tài)不會影響目標(biāo)平臺上的正確執(zhí)行,則不需要禁止引入數(shù)據(jù)競態(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 不會出現(xiàn)讀取出錯,因為潛在的讀取不會影響任何現(xiàn)有的并發(fā)讀取或?qū)懭搿?/p>

結(jié)論

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

筆者寄語

本文翻譯自官方 博客原文[1], 希望讀者在讀完本文后,能夠深入理解 happens before 在各場景下的規(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對象內(nèi)存

2024-05-20 11:33:20

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

2024-03-11 00:09:00

模型融合場景

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工具

2023-08-10 08:28:46

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

2022-05-24 08:21:16

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

2023-06-30 08:18:51

敏捷開發(fā)模式

2023-09-10 21:42:31

2024-09-05 10:36:58

2024-06-27 08:54:22

Go模塊團隊

2024-05-17 08:47:33

數(shù)組切片元素

2022-11-12 12:33:38

CSS預(yù)處理器Sass

2023-12-28 09:55:08

隊列數(shù)據(jù)結(jié)構(gòu)存儲

2022-10-28 07:27:17

Netty異步Future

2022-06-26 09:40:55

Django框架服務(wù)

2023-07-27 07:46:51

SAFe團隊測試
點贊
收藏

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