亚欧色一区w666天堂,色情一区二区三区免费看,少妇特黄A片一区二区三区,亚洲人成网站999久久久综合,国产av熟女一区二区三区

  • 發布文章
  • 消息中心
點贊
收藏
評論
分享
原創

Golang 內存模型詳解

2023-05-29 05:46:03
59
0

寫在前面的話

數據競態:當存在兩個或以上的goroutine,對一個數據,同時進行讀寫操作時(非原子操作),我們稱之為該數據存在竟態。在竟態條件下,讀者可能讀到非一致性的數據:比如一個結構體數據,goroutine1 正在讀v1版的數據;此時goroutine2 進行數據更新到v2;導致goroutine1讀取到的數據部分是v1版的數據,部分是v2版的數據。

當多個goroutine操作存在竟態的數據時,必須將并發操作進行順序化,避免出現數據不一致。在golang中可以實現操作順序化的方法有:基于通道(channel)通信,使用同步原語(如sync包中的Mutex、RWMutex等),使用原子操作(sync/atomic)等。

另一方面,Golang內存模型,定義了一些限制條件,當程序遵循這些限制條件時,就可以實現數據免竟態。這些限制條件被稱之為“Hanpens Before”規則。我們將在下一章重點說明。

警告:

理解和掌握golang 的 Hanpens Before規則,有助于我們了解哪些情況下,數據的可見性是有保障,而哪些情況下是不確定的的,從而避免跌入數據不可見的迷障。另一方面,我們在設計和實現程序時,不要過度依賴golang的Hanpens Before規則,而使程序變得晦澀難懂。

 

Hanpens Before (先行發生)

理解Hanpens Before

為了提高程序執行時的效率,編譯器和處理器常常會對指令做重排序。重排序分三種類型:
(1)編譯器優化的重排序:編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
(2)指令級并行的重排序:現代處理器采用了指令級并行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
(3)內存系統的重排序: 由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。


在一個goroutine中,讀和寫必須按照程序指定的順序執行。也就是說,只有當指令重排沒有改變既定的代碼的邏輯順序時,編譯器和處理器才可以重新排序在單個goroutine中執行的讀寫操作。反過來講,只要程序的既定邏輯沒有被改變,那么編譯器和處理器就可以按需要對指令進行重新排序。由于這種重新排序,一個goroutine觀察到的執行順序可能與另一個goroutine感知到的執行順序不同。例如:

有全局變量a 和b,在goroutine1 中有如下代碼:

// in goroutine1

a = 10         // action 1
b = 1          // action 2
c = a + b     // action 3

fmt.Println(c)  // action 4

同時,在goroutine2 中,讀取a和b的內容:

// in goroutine2

for {
    if b == 1 {
        fmt.Println(a)
        break
    }
}

按程序表達,goroutine2中,當 b == 1 達成時,a應該已經被賦值了,程序期望輸出 a的值是10。 但實際運行時,卻發現輸出a 的值有時是10,有時是0。 這是因為,在goroutine1 中,a和b的賦值操作,并沒有邏輯依賴關系,所以編譯器和處理器可以對該兩條指令進行重新排序,導致實際b=1 可能先于a =10 被執行;此時goroutine2中,看到b==1 已經達成,但此時a卻依舊保持初始值0(這里有兩種情況會導致看見a==0,其一,goroutine1 尚未執行a=10;其二,goroutine1執行了a=10,但由于CPU cache 緩存,goroutine2 并未取到更新后的a的值)。

這種場景下,我們說:

action 1 Hanpens Before action 2 條件不必要”。

當 Hanpens Before 條件不滿足,且沒有額外引入同步機制確保操作順序時,程序就可能出現未知狀態。例如一個經典范式:

var(
 inited = false
 config *Config
)

func doInit() {
    c := loadConfig()
    if c == nil {
        panic("load config error")
   }
   config =c
   inited = true
}

func main() {
    go doInit()
    for !inited {
         // wait init done
     }
    doWithConfig(config)
}

上面的例子,是一個經典的異步初始化的模型范例,甚至在一些生產代碼也可以見到同樣的應用。但是,上面的程序,有可能出現在main流程中,等待初始化成功后,執行后面操作時,卻得到config是一個空指針而引發程序崩潰。

 

繼續回到本節開篇的例子,在goroutine1 中對 a,b,c 進行賦值,現在有goroutine3 ,代碼如下:

// in goroutine 3

for {
     if c == 11 {
          fmt.Println(a)
          break
    }
}

運行程序發現,goroutine3 打印a 的值是明確的,都是10。這是因為,在goroutine1 中, c= a+b 要求在對c 賦值之前,a 和b 必須要完成賦值。即:

action 1 和action 2 Hanpens Before action 3 條件必要

 

總結

Hanpens Before 描述了兩個行為的明確的先后關系: actionA 先于 actionB 發生。這種關系,通常是有一些基礎策略,邏輯依賴,實現原理所決定的。比如,golang中,程序啟動的流程為:

(1)從main 包開始執行,先引入所依賴的包;所述依賴包還有依賴,則遞歸引入依賴;

(2)引入依賴包后,執行該依賴包的init函數(如果有);

(3)從main 包的main函數開始執行。

通過以上流程,不難得出:

(1)加載程序所有依賴包  Hanpens Before  main()函數開始執行;

(2)依賴包的init函數執行完畢  Hanpens Before main() 函數開始執行;

(3)在加載過程中,軟件包完成其依賴包的加載 Hanpens Before 該軟件包init()函數開始執行。

 

另一方面,當兩個行為或過程滿足 Hanpens Before 的必要條件時,說明他們的執行順序是明確有序的,它們就不會出現數據靜態,無需額外的同步機制。

下面我們將詳細說明,golang中那些過程是滿足Hanpens Before 條件的。

 

golang 中符合Hanpens Before的過程組合

Go的初始化

程序初始化運行在單個goroutine(主協程)中,并且該goroutine能夠創立其余并發運行的goroutine。

  • 如果包p導入了包q,則q包init函數執行完結 先于 p包init函數的執行。
  • main函數的執行發生在所有init函數執行完成之后。

goroutine的創建和退出

  • goroutine的創建先于goroutine的執行。
  • 注意)goroutine的退出不與程序中的任何過程形成Hanpens Before必要條件;即goroutine的退出是無法預測的。如果用一個goroutine察看另一個goroutine的退出,請應用鎖(waitgroup)或者Channel來保障絕對有序。

關于goroutine的創建 先于 goroutine 的執行,感覺上像句廢話;個人理解是:創建goroutine 的語句(尤其是復合語句)的執行,一定先于goroutine開始執行之前。舉例:

func delay( sec int) int {
    time.Sleep(time.Second * sec)
    return  sec
}

go fmt.Println(delay(1))

上述代碼執行時,創建goroutine的過程會因為調用delay 函數而阻塞 main goroutine 一秒鐘。

channel的發送和接收

  • 對channel 的send 動作開始  先于 對channel的接收完成

見下面的示例代碼:

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

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

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

上面的程序 打印的輸出一定是“heallo,world”。因為主協程是在讀取到管道返回值后,才讀取a的值;當管道能夠讀取到值時,寫管道的動作一定先前發生了;f協程中,對a賦值先于寫channel(這點是程序的檢測點機制,不展開);因而可以推論出:對a的賦值 先于 對a的讀取。因此該程序的讀寫流程是嚴格同步的。

 

  • 對channel的關閉動作 先于 讀端因為channel已關閉而讀到一個零值

見下面的示例代碼:

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

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

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

上面的程序 打印的輸出一定是“heallo,world”。因為讀取端獲取到返回值時,f協程的close動作一定先發生了。

 

  • 在一個無緩存的channel上,接收完成 先于 發送完成
見下面的示例:
var c = make(chan int)
var a string


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


func main() {
    go f()
    c <- 0
    print(a)
}
上面的程序 打印的輸出一定是“heallo,world”。因為主協程通過無緩沖channel向 f 協程寫入,當主協程寫入完成返回時,f協程的接收必定先于此完成,故而對a的寫入和讀取是嚴格同步的。
 
  • 容量為C的channel,第k個接收完成 先于 第k +C個發送完成前

本質上,是帶緩沖的channel當buffer滿后,發生端會阻塞等待一個buffer空閑后、完成寫入后返回。應用該原理,一個限制并發數的調度隊列可以簡單實現為:

var limit = make(chan int, 3)

func scheduleWorker(w interface{}) {
    limit <- 1
    w()
    <-limit
}

func main() {
	for _, w := range work {
		go scheduleWorker(w)
	}
	select{}
}

上面代碼, scheduleWorker中,worker 被調度后,先嘗試往channel中寫入一個標記;如果此時buffer 未滿則可以順利寫入(類似于獲取令牌),程序繼續執行真正的worker任務;相反,如果channel的buffer已滿(已有三個任務正在執行),那么當前任務會阻塞在channel的寫入上,直到其他任務執行完畢后,從channel中讀取一個數據(類似于補充令牌),觸發寫端繼續寫入。

 

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

見下面示例代碼:

var l sync.Mutex
var a string

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

func main() {
	l.Lock()   // 第一次鎖
	go f()     // 執行一次解鎖
	l.Lock()   // 第二次鎖
	print(a)
}

上面程序輸出a 的值“hello, world”。主協程中,第一次獲取鎖成功,當第二次再嘗試獲取鎖時,程序被阻塞,直到鎖狀態被Unlock解除。這樣,a的寫入和讀取是嚴格有序的。

 

  • 對于讀寫鎖l(sync.RWMutex),當有寫鎖存在時,任意的讀加鎖(l.RLock)返回 在 所有寫鎖解鎖(l.Unlock)完成之后
  • 對于讀寫鎖l(sync.RWMutex), 當有讀鎖存在時,任意的寫加鎖(l.Lock)返回 在 所有的讀鎖解鎖(l.RUnlock)完成之后

總結讀寫鎖的持鎖規則:

(1)當存在讀加鎖(l.Rlock)時,新的讀加鎖無需等待之前的讀鎖解鎖,即可并行持鎖(相較與互斥鎖的主要改進);

(2)當存在讀加鎖(l.Rlock)時,寫鎖(l.Lock)必須等待所有的讀鎖解鎖(l.RUnlock)完成后,方能持鎖;

(3)當存在寫加鎖(l.Lock)時,讀鎖(l.Rlock)必須等待所有的寫鎖解鎖(l.Unlock)完成后,方能持鎖;

(4)當存在寫加鎖(l.Lock)時,寫鎖(l.Lock)必須等待所有的寫鎖解鎖(l.Unlock)完成后,方能持鎖。

 

Once

  • 使用once進行初始化,once.do(f)的返回,一定在 f函數執行完成之后。

 once本質上是一個封裝的單例模式,內部采用了互斥鎖,確保多個goroutine 并發調用once.do進行初始化,其中只有一個協程可以獲取到執行f的權限;其余協程都等待f執行完成后方可以返回。

 

原子操作

  • sync/atomic中提供的API 可以在并發協程中調用而保持嚴格的可見性。  

 

Finalizers

  • 通過runtime包對實例設置 finalizers函數,SetFinalizer(x, f) 完成 先于 f 函數的執

這個與創建協程的邏輯一致。

0條評論
0 / 1000
huskar
20文章數
3粉絲數
huskar
20 文章 | 3 粉絲
原創

Golang 內存模型詳解

2023-05-29 05:46:03
59
0

寫在前面的話

數據競態:當存在兩個或以上的goroutine,對一個數據,同時進行讀寫操作時(非原子操作),我們稱之為該數據存在竟態。在竟態條件下,讀者可能讀到非一致性的數據:比如一個結構體數據,goroutine1 正在讀v1版的數據;此時goroutine2 進行數據更新到v2;導致goroutine1讀取到的數據部分是v1版的數據,部分是v2版的數據。

當多個goroutine操作存在竟態的數據時,必須將并發操作進行順序化,避免出現數據不一致。在golang中可以實現操作順序化的方法有:基于通道(channel)通信,使用同步原語(如sync包中的Mutex、RWMutex等),使用原子操作(sync/atomic)等。

另一方面,Golang內存模型,定義了一些限制條件,當程序遵循這些限制條件時,就可以實現數據免竟態。這些限制條件被稱之為“Hanpens Before”規則。我們將在下一章重點說明。

警告:

理解和掌握golang 的 Hanpens Before規則,有助于我們了解哪些情況下,數據的可見性是有保障,而哪些情況下是不確定的的,從而避免跌入數據不可見的迷障。另一方面,我們在設計和實現程序時,不要過度依賴golang的Hanpens Before規則,而使程序變得晦澀難懂。

 

Hanpens Before (先行發生)

理解Hanpens Before

為了提高程序執行時的效率,編譯器和處理器常常會對指令做重排序。重排序分三種類型:
(1)編譯器優化的重排序:編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
(2)指令級并行的重排序:現代處理器采用了指令級并行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
(3)內存系統的重排序: 由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。


在一個goroutine中,讀和寫必須按照程序指定的順序執行。也就是說,只有當指令重排沒有改變既定的代碼的邏輯順序時,編譯器和處理器才可以重新排序在單個goroutine中執行的讀寫操作。反過來講,只要程序的既定邏輯沒有被改變,那么編譯器和處理器就可以按需要對指令進行重新排序。由于這種重新排序,一個goroutine觀察到的執行順序可能與另一個goroutine感知到的執行順序不同。例如:

有全局變量a 和b,在goroutine1 中有如下代碼:

// in goroutine1

a = 10         // action 1
b = 1          // action 2
c = a + b     // action 3

fmt.Println(c)  // action 4

同時,在goroutine2 中,讀取a和b的內容:

// in goroutine2

for {
    if b == 1 {
        fmt.Println(a)
        break
    }
}

按程序表達,goroutine2中,當 b == 1 達成時,a應該已經被賦值了,程序期望輸出 a的值是10。 但實際運行時,卻發現輸出a 的值有時是10,有時是0。 這是因為,在goroutine1 中,a和b的賦值操作,并沒有邏輯依賴關系,所以編譯器和處理器可以對該兩條指令進行重新排序,導致實際b=1 可能先于a =10 被執行;此時goroutine2中,看到b==1 已經達成,但此時a卻依舊保持初始值0(這里有兩種情況會導致看見a==0,其一,goroutine1 尚未執行a=10;其二,goroutine1執行了a=10,但由于CPU cache 緩存,goroutine2 并未取到更新后的a的值)。

這種場景下,我們說:

action 1 Hanpens Before action 2 條件不必要”。

當 Hanpens Before 條件不滿足,且沒有額外引入同步機制確保操作順序時,程序就可能出現未知狀態。例如一個經典范式:

var(
 inited = false
 config *Config
)

func doInit() {
    c := loadConfig()
    if c == nil {
        panic("load config error")
   }
   config =c
   inited = true
}

func main() {
    go doInit()
    for !inited {
         // wait init done
     }
    doWithConfig(config)
}

上面的例子,是一個經典的異步初始化的模型范例,甚至在一些生產代碼也可以見到同樣的應用。但是,上面的程序,有可能出現在main流程中,等待初始化成功后,執行后面操作時,卻得到config是一個空指針而引發程序崩潰。

 

繼續回到本節開篇的例子,在goroutine1 中對 a,b,c 進行賦值,現在有goroutine3 ,代碼如下:

// in goroutine 3

for {
     if c == 11 {
          fmt.Println(a)
          break
    }
}

運行程序發現,goroutine3 打印a 的值是明確的,都是10。這是因為,在goroutine1 中, c= a+b 要求在對c 賦值之前,a 和b 必須要完成賦值。即:

action 1 和action 2 Hanpens Before action 3 條件必要

 

總結

Hanpens Before 描述了兩個行為的明確的先后關系: actionA 先于 actionB 發生。這種關系,通常是有一些基礎策略,邏輯依賴,實現原理所決定的。比如,golang中,程序啟動的流程為:

(1)從main 包開始執行,先引入所依賴的包;所述依賴包還有依賴,則遞歸引入依賴;

(2)引入依賴包后,執行該依賴包的init函數(如果有);

(3)從main 包的main函數開始執行。

通過以上流程,不難得出:

(1)加載程序所有依賴包  Hanpens Before  main()函數開始執行;

(2)依賴包的init函數執行完畢  Hanpens Before main() 函數開始執行;

(3)在加載過程中,軟件包完成其依賴包的加載 Hanpens Before 該軟件包init()函數開始執行。

 

另一方面,當兩個行為或過程滿足 Hanpens Before 的必要條件時,說明他們的執行順序是明確有序的,它們就不會出現數據靜態,無需額外的同步機制。

下面我們將詳細說明,golang中那些過程是滿足Hanpens Before 條件的。

 

golang 中符合Hanpens Before的過程組合

Go的初始化

程序初始化運行在單個goroutine(主協程)中,并且該goroutine能夠創立其余并發運行的goroutine。

  • 如果包p導入了包q,則q包init函數執行完結 先于 p包init函數的執行。
  • main函數的執行發生在所有init函數執行完成之后。

goroutine的創建和退出

  • goroutine的創建先于goroutine的執行。
  • 注意)goroutine的退出不與程序中的任何過程形成Hanpens Before必要條件;即goroutine的退出是無法預測的。如果用一個goroutine察看另一個goroutine的退出,請應用鎖(waitgroup)或者Channel來保障絕對有序。

關于goroutine的創建 先于 goroutine 的執行,感覺上像句廢話;個人理解是:創建goroutine 的語句(尤其是復合語句)的執行,一定先于goroutine開始執行之前。舉例:

func delay( sec int) int {
    time.Sleep(time.Second * sec)
    return  sec
}

go fmt.Println(delay(1))

上述代碼執行時,創建goroutine的過程會因為調用delay 函數而阻塞 main goroutine 一秒鐘。

channel的發送和接收

  • 對channel 的send 動作開始  先于 對channel的接收完成

見下面的示例代碼:

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

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

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

上面的程序 打印的輸出一定是“heallo,world”。因為主協程是在讀取到管道返回值后,才讀取a的值;當管道能夠讀取到值時,寫管道的動作一定先前發生了;f協程中,對a賦值先于寫channel(這點是程序的檢測點機制,不展開);因而可以推論出:對a的賦值 先于 對a的讀取。因此該程序的讀寫流程是嚴格同步的。

 

  • 對channel的關閉動作 先于 讀端因為channel已關閉而讀到一個零值

見下面的示例代碼:

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

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

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

上面的程序 打印的輸出一定是“heallo,world”。因為讀取端獲取到返回值時,f協程的close動作一定先發生了。

 

  • 在一個無緩存的channel上,接收完成 先于 發送完成
見下面的示例:
var c = make(chan int)
var a string


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


func main() {
    go f()
    c <- 0
    print(a)
}
上面的程序 打印的輸出一定是“heallo,world”。因為主協程通過無緩沖channel向 f 協程寫入,當主協程寫入完成返回時,f協程的接收必定先于此完成,故而對a的寫入和讀取是嚴格同步的。
 
  • 容量為C的channel,第k個接收完成 先于 第k +C個發送完成前

本質上,是帶緩沖的channel當buffer滿后,發生端會阻塞等待一個buffer空閑后、完成寫入后返回。應用該原理,一個限制并發數的調度隊列可以簡單實現為:

var limit = make(chan int, 3)

func scheduleWorker(w interface{}) {
    limit <- 1
    w()
    <-limit
}

func main() {
	for _, w := range work {
		go scheduleWorker(w)
	}
	select{}
}

上面代碼, scheduleWorker中,worker 被調度后,先嘗試往channel中寫入一個標記;如果此時buffer 未滿則可以順利寫入(類似于獲取令牌),程序繼續執行真正的worker任務;相反,如果channel的buffer已滿(已有三個任務正在執行),那么當前任務會阻塞在channel的寫入上,直到其他任務執行完畢后,從channel中讀取一個數據(類似于補充令牌),觸發寫端繼續寫入。

 

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

見下面示例代碼:

var l sync.Mutex
var a string

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

func main() {
	l.Lock()   // 第一次鎖
	go f()     // 執行一次解鎖
	l.Lock()   // 第二次鎖
	print(a)
}

上面程序輸出a 的值“hello, world”。主協程中,第一次獲取鎖成功,當第二次再嘗試獲取鎖時,程序被阻塞,直到鎖狀態被Unlock解除。這樣,a的寫入和讀取是嚴格有序的。

 

  • 對于讀寫鎖l(sync.RWMutex),當有寫鎖存在時,任意的讀加鎖(l.RLock)返回 在 所有寫鎖解鎖(l.Unlock)完成之后
  • 對于讀寫鎖l(sync.RWMutex), 當有讀鎖存在時,任意的寫加鎖(l.Lock)返回 在 所有的讀鎖解鎖(l.RUnlock)完成之后

總結讀寫鎖的持鎖規則:

(1)當存在讀加鎖(l.Rlock)時,新的讀加鎖無需等待之前的讀鎖解鎖,即可并行持鎖(相較與互斥鎖的主要改進);

(2)當存在讀加鎖(l.Rlock)時,寫鎖(l.Lock)必須等待所有的讀鎖解鎖(l.RUnlock)完成后,方能持鎖;

(3)當存在寫加鎖(l.Lock)時,讀鎖(l.Rlock)必須等待所有的寫鎖解鎖(l.Unlock)完成后,方能持鎖;

(4)當存在寫加鎖(l.Lock)時,寫鎖(l.Lock)必須等待所有的寫鎖解鎖(l.Unlock)完成后,方能持鎖。

 

Once

  • 使用once進行初始化,once.do(f)的返回,一定在 f函數執行完成之后。

 once本質上是一個封裝的單例模式,內部采用了互斥鎖,確保多個goroutine 并發調用once.do進行初始化,其中只有一個協程可以獲取到執行f的權限;其余協程都等待f執行完成后方可以返回。

 

原子操作

  • sync/atomic中提供的API 可以在并發協程中調用而保持嚴格的可見性。  

 

Finalizers

  • 通過runtime包對實例設置 finalizers函數,SetFinalizer(x, f) 完成 先于 f 函數的執

這個與創建協程的邏輯一致。

文章來自個人專欄
文章 | 訂閱
0條評論
0 / 1000
請輸入你的評論
0
0