Go語言中利用共享內(nèi)存進行線程間通信
多個執(zhí)行線程協(xié)作解決問題時,需要進行通信,即線程間通信;進程之間的通信則稱為進程間通信。這兩種通信方式主要分為兩類:內(nèi)存共享與消息傳遞。本章重點討論前者。
內(nèi)存共享的原理類似于讓所有執(zhí)行過程共享一個公共空間,即進程地址空間,每個進程都可在此空間內(nèi)存儲其計算結(jié)果。通過協(xié)調(diào)各進程,可利用此公共空間實現(xiàn)協(xié)作。消息傳遞則如其名稱所示,線程通過互相發(fā)送消息進行通信。
共享內(nèi)存

在Go語言中,多個goroutine可運行于多個內(nèi)核級線程上,因此用于運行多線程應用程序的硬件與操作系統(tǒng)架構(gòu)必須支持進程內(nèi)各線程間的內(nèi)存共享。在單處理器系統(tǒng)中,架構(gòu)相對簡單:同一進程中的所有內(nèi)核級線程都有權(quán)訪問相同內(nèi)存區(qū)域,通過上下文切換進行讀取與寫入。在多處理器或多核系統(tǒng)中,情況變得復雜,因為CPU與主存之間通常存在多層緩存。
隨著處理器數(shù)量增加,系統(tǒng)總線的負載也會增大,成為系統(tǒng)擴展的瓶頸。為了減輕總線壓力,可利用緩存將數(shù)據(jù)存儲在離CPU更近的位置,從而提高性能。緩存減少了數(shù)據(jù)從主存讀取的頻率,從而也降低了總線負載。下圖展示了這種機制。

這是一種簡化架構(gòu),僅包含兩個CPU與單層緩存。現(xiàn)代架構(gòu)通常包含更多處理器與多層緩存。
下圖展示了兩個線程通過共享內(nèi)存進行通信的場景。假設線程1嘗試從主存讀取某個變量。系統(tǒng)會將包含該變量的內(nèi)存塊內(nèi)容復制到距CPU更近的緩存中。當線程1再次讀寫此變量時,便可通過緩存快速完成,無需再次訪問系統(tǒng)總線。

現(xiàn)假設線程1決定更新該變量的值,從而更新了緩存中的數(shù)據(jù)。若不采取任何措施,線程2看到的將是舊數(shù)據(jù)。
一種解決方案是采用緩存寫通機制:當線程1更新緩存時,該更新會同步至主存。然而,若線程2在另一本地緩存中持有該數(shù)據(jù)的舊副本,問題依然存在。為了解決此問題,可讓緩存監(jiān)聽內(nèi)存更新事件。當緩存檢測到其持有的數(shù)據(jù)已更新時,或應用此更新,或直接使該緩存項失效。若選擇失效,則后續(xù)線程再次需要該數(shù)據(jù)時,必須從主存獲取最新版本。下圖展示了這一過程。

在多處理器系統(tǒng)中,規(guī)范對內(nèi)存與緩存數(shù)據(jù)讀寫訪問的機制稱為緩存一致性協(xié)議。前述“寫回并使副本失效”策略即為該協(xié)議的一種實現(xiàn)?,F(xiàn)代計算機架構(gòu)通常會組合使用多種協(xié)議以保證數(shù)據(jù)一致性。
一致性墻
微芯片工程師擔憂隨著處理器核心數(shù)量的增長,緩存一致性將成為性能瓶頸。當核心數(shù)量大幅增加時,實現(xiàn)一致性的復雜性與成本將急劇上升,最終制約性能提升,這被稱為一致性瓶頸。
協(xié)程之間共享變量
如何讓兩個協(xié)程共享內(nèi)存?我們可以創(chuàng)建一個協(xié)程,使其與執(zhí)行main()函數(shù)的主線程共享某個內(nèi)存變量。例如,此變量可用作倒計時器:一個協(xié)程每秒減少該變量,另一個協(xié)程則定期讀取并將其顯示在控制臺上。

package main
import (
"fmt"
"time"
)
func countdown(seconds *int) {
for *seconds > 0 {
time.Sleep(1 * time.Second)
*seconds -= 1
}
}
func main() {
// 聰明的go編譯器會自動識別到count需要存儲在堆上,會自動將count放置到堆上
count := 5
go countdown(&count)
for count > 0 {
time.Sleep(500 * time.Millisecond)
fmt.Println(count)
}
}由于我們讀取共享變量的頻率高于更新它的頻率,因此相同的值會在控制臺輸出中重復出現(xiàn)多次。
逃逸分析
Go 編譯器需為變量 count 決定存儲位置:分配在函數(shù)棧幀或進程的主內(nèi)存堆上。每個線程擁有獨立的棧,但共享進程的堆內(nèi)存。當一個新 goroutine 執(zhí)行 countdown() 時,count 變量無法存在于 main() 的棧上,因為 Go 運行時禁止一個 goroutine 訪問另一個 goroutine 的棧數(shù)據(jù),兩者生命周期可能不同。當編譯器識別出變量需要在 goroutine 間共享時,會將其分配在堆上,即便它在語法上看起來像局部變量。
定義:在技術上,當一個變量在聲明時理應位于函數(shù)棧上,但最終被分配在堆上,則稱其“逃逸”到堆中。逃逸分析是編譯器判斷變量應分配在堆而非棧上的算法。
變量在多種情況下會被分配到堆上。一旦變量被共享至其函數(shù)棧幀之外,便會存儲在堆中。例如,在多協(xié)程間共享變量引用即屬此類,如下圖所示。
定義:從技術上講,當我們聲明一個變量時,如果該變量看似應該被存儲在局部函數(shù)的棧內(nèi)存中,但實際上卻被分配到了堆內(nèi)存中,那么我們就說這個變量“逃逸”到了堆內(nèi)存中。所謂“逃逸分析”,其實就是編譯器用來判斷某個變量是否應該被存儲在堆內(nèi)存而非棧內(nèi)存中的算法。
在很多情況下,變量會被分配到堆上。每當一個變量被共享到函數(shù)棧幀之外時,該變量就會被存儲在堆中。例如,在多個協(xié)程之間共享某個變量的引用,就是這種情況,如圖所示。

在 Go 中,使用堆內(nèi)存相比棧內(nèi)存會帶來額外開銷,因為不再使用的堆內(nèi)存需要由垃圾回收器清理。垃圾回收器會遍歷堆,標記不再被任何 goroutine 引用的對象以供回收。
與之相對,棧內(nèi)存則在函數(shù)執(zhí)行完畢后自動回收。
可通過編譯器選項 -m 查看其優(yōu)化決策,包括變量是否逃逸至堆。
go build -gcflags="-m" countdown.go # command-line-arguments ./countdown.go:11:6: can inline countdown ./countdown.go:20:5: can inline main.gowrap1 ./countdown.go:23:20: inlining call to fmt.Println ./countdown.go:20:17: inlining call to countdown ./countdown.go:11:16: seconds does not escape ./countdown.go:19:5: moved to heap: count ./countdown.go:23:20: ... argument does not escape ./countdown.go:23:21: count escapes to heap
編譯器輸出顯示:第11行的參數(shù) seconds 未逃逸,仍位于 countdown() 函數(shù)的棧上;但需與其他 goroutine 共享的 count 變量被移至堆上。
若移除 go 關鍵字改造為順序程序,編譯器則不再將 count 移至堆上。修改后的輸出如下:
package main
import (
"fmt"
"time"
)
func countdown(seconds *int) {
for *seconds > 0 {
time.Sleep(1 * time.Second)
*seconds -= 1
}
}
func main() {
count := 5
// 這里不再使用協(xié)程,count就變成在同一個協(xié)程里面共享的變量,不會進行內(nèi)存逃逸
countdown(&count)
for count > 0 {
time.Sleep(500 * time.Millisecond)
fmt.Println(count)
}
}
go build -gcflags="-m" countdown.go # command-line-arguments // 將countdown函數(shù)進行內(nèi)聯(lián)處理 ./countdown.go:12:6: can inline countdown ./countdown.go:21:11: inlining call to countdown ./countdown.go:24:14: inlining call to fmt.Println ./countdown.go:12:16: seconds does not escape ./countdown.go:24:14: ... argument does not escape ./countdown.go:24:15: count escapes to heap
注意,count 不再有“移至堆”的提示,且有信息顯示 countdown() 的調(diào)用被內(nèi)聯(lián)處理。內(nèi)聯(lián)是指在特定條件下,編譯器將被調(diào)用函數(shù)的代碼直接插入調(diào)用點以替代函數(shù)調(diào)用,這能減少函數(shù)調(diào)用開銷——包括準備新棧幀、傳遞參數(shù)和指令跳轉(zhuǎn)。但是,當函數(shù)在獨立棧上并發(fā)執(zhí)行時,內(nèi)聯(lián)失去意義。
使用 goroutine 會舍棄某些編譯器優(yōu)化(如內(nèi)聯(lián)),同時因共享變量存儲在堆上而增加運行時開銷。不過,通過并行執(zhí)行,仍可能獲得性能提升。
在多個協(xié)程中更新共享變量
下面討論涉及超過兩個協(xié)程同時修改同一變量的例子。我們將編寫一個程序來統(tǒng)計網(wǎng)頁文本中的英文字母頻率。程序下載多個網(wǎng)頁,分別統(tǒng)計字母出現(xiàn)次數(shù),最終匯總輸出。
我們先實現(xiàn)順序版本,再改造為并發(fā)版本。程序步驟與數(shù)據(jù)結(jié)構(gòu)如下圖所示。我們使用一個整數(shù)切片作為字母計數(shù)表。程序依次處理網(wǎng)頁列表中的每個網(wǎng)頁,下載、分析內(nèi)容并更新字母計數(shù)。

package main
import (
"fmt"
"io"
"net/http"
"strings"
)
const allLetters = "abcdefghijklmnopqrstuvwxyz"
func countLetters(url string, frequency []int) {
resp, _ := http.Get(url)
defer resp.Body.Close()
if resp.StatusCode != 200 {
panic("Server returning error status code: " + resp.Status)
}
body, _ := io.ReadAll(resp.Body)
for _, b := range body {
c := strings.ToLower(string(b))
cIndex := strings.Index(allLetters, c)
if cIndex >= 0 {
frequency[cIndex] += 1
}
}
fmt.Println("Completed:", url)
}
func main() {
var frequency = make([]int, 26)
for i := 1000; i <= 1030; i++ {
url := fmt.Sprintf("https://rfc-editor.org/rfc/rfc%d.txt", i)
countLetters(url, frequency)
}
for i, c := range allLetters {
fmt.Printf("%c-%d ", c, frequency[i])
}
}
該函數(shù)首先下載指定URL的文檔,然后遍歷其內(nèi)容,將字符統(tǒng)一轉(zhuǎn)為小寫,并在字母表中查找相應索引以累加計數(shù),最終輸出處理完成的URL。
我們可以利用 go 關鍵字讓 countLetters() 函數(shù)并行執(zhí)行,并發(fā)下載處理網(wǎng)頁。

警告:多個協(xié)程同時調(diào)用 countLetters() 會因“競態(tài)條件”導致計數(shù)錯誤。此例僅用于演示。
package main
import (
"fmt"
"io"
"net/http"
"strings"
"time"
)
const allLetters = "abcdefghijklmnopqrstuvwxyz"
/*
Note: this program has a race condition for demonstration purposes
Additionally we have a timer at the end which you might need to adjust
depending on how fast your internet connection is.
In later chapters we cover how to wait for threads to complete their work
*/
func countLetters(url string, frequency []int) {
resp, _ := http.Get(url)
defer resp.Body.Close()
if resp.StatusCode != 200 {
panic("Server returning error status code: " + resp.Status)
}
body, _ := io.ReadAll(resp.Body)
for _, b := range body {
c := strings.ToLower(string(b))
cIndex := strings.Index(allLetters, c)
if cIndex >= 0 {
frequency[cIndex] += 1
}
}
fmt.Println("Completed:", url)
}
func main() {
var frequency = make([]int, 26)
for i := 1000; i <= 1030; i++ {
url := fmt.Sprintf("https://rfc-editor.org/rfc/rfc%d.txt", i)
go countLetters(url, frequency)
}
time.Sleep(10 * time.Second)
for i, c := range allLetters {
fmt.Printf("%c-%d ", c, frequency[i])
}
}
首先,并發(fā)下載比順序下載快得多。其次,完成消息的順序也變得無序,因為各網(wǎng)頁大小不同,下載結(jié)束時間各異。但頁面處理順序?qū)ψ罱K統(tǒng)計結(jié)果無影響。
問題在于結(jié)果:并發(fā)處理與順序處理統(tǒng)計的各字母頻率存在差異。例如,字母“e”在順序統(tǒng)計中出現(xiàn)181,360次,而在某次并發(fā)統(tǒng)計中僅出現(xiàn)179,936次。多次運行程序,順序版本始終得到一致結(jié)果,并發(fā)版本每次結(jié)果都有偏差。原因在于競態(tài)條件。當多個線程未經(jīng)協(xié)調(diào)地共享資源時,會產(chǎn)生不可預期的結(jié)果。下一章節(jié)將解決此問題。
競態(tài)條件
競態(tài)條件是指程序在嘗試并發(fā)執(zhí)行多任務時,其運行結(jié)果取決于不可預測事件發(fā)生的精確時序。正如上一節(jié)的示例所示,程序可能偶爾得出錯誤結(jié)果;嚴重時,代碼可能長期穩(wěn)定后突然崩潰,導致數(shù)據(jù)損壞。這是因為缺乏同步機制來協(xié)調(diào)并發(fā)任務。
吝嗇與揮霍:如何制造競爭條件
“吝嗇鬼”與“揮霍者”是兩個獨立的線程,使用同一個銀行賬戶。吝嗇鬼工作賺錢,揮霍者消費金錢。兩者各自重復進行100萬次操作,吝嗇鬼每次加10,揮霍者每次減10。理論上賬戶應保持初始值100不變。

package main
import (
"fmt"
"time"
)
/*
Note: this program has a race condition for demonstration purposes
In later chapters we cover how to wait for threads to complete their work
*/
func stingy(money *int) {
for i := 0; i < 1000000; i++ {
*money += 10
}
fmt.Println("Stingy Done")
}
func spendy(money *int) {
for i := 0; i < 1000000; i++ {
*money -= 10
}
fmt.Println("Spendy Done")
}
func main() {
money := 100
go stingy(&money)
go spendy(&money)
time.Sleep(2 * time.Second)
fmt.Println("Money in bank account: ", money)
}
我們將普通銀行賬戶的初始金額設置為100美元。同時,在創(chuàng)建了各個goroutine之后,主線程會暫停2秒鐘,等待這些goroutine執(zhí)行完畢。當主線程重新開始運行時,它會打印出money變量中所存儲的金額。
這是一次幸運的結(jié)果。再次運行可能得到負數(shù)。這正是競態(tài)條件導致的典型錯誤。
競態(tài)條件是“海森堡錯誤”的一個典型例子,此類錯誤難以復現(xiàn)和調(diào)試,因為調(diào)試行為本身(如設置斷點)可能改變其發(fā)生條件。防范競態(tài)條件的最佳策略是在開發(fā)時就避免其發(fā)生。
以下場景展示了問題根源。為簡化,假設只有一個處理器,因此并非真正并行。下圖展示了時間線交錯時可能出現(xiàn)的數(shù)據(jù)不一致。在時間戳7到11之間,兩個線程通過寄存器緩存了共享變量,在進行加減操作后寫回時,相互覆蓋導致數(shù)據(jù)異常。
定義:“原子”意味著不可分割。原子操作是無法被中斷的操作。
問題的根本原因是 *money += 10 和 *money -= 10 并非原子操作,它們會被編譯為多條指令,在這些指令間可能發(fā)生中斷,或被其他 goroutine 的指令干擾,進而引發(fā)競態(tài)條件。
定義:臨界區(qū)是指一段不能受到其他并發(fā)執(zhí)行流干擾的指令序列,否則可能影響數(shù)據(jù)狀態(tài)并導致競態(tài)條件。
即使指令是原子的,問題依然可能出現(xiàn)。處理器緩存與寄存器會緩存常用變量,編譯器會優(yōu)化以將其保留在CPU寄存器中,而不立即寫回主存。不同并行協(xié)程可能因此看到相同變量的不同副本,因為各CPU核心在定期同步內(nèi)存前,無法感知其他核心的修改。
編寫不當?shù)牟l(fā)程序在并行環(huán)境中運行時,發(fā)生此類問題的風險更高。若使用協(xié)程且系統(tǒng)只運行在單個處理器上,由于用戶級調(diào)度的非搶占性,指令執(zhí)行過程發(fā)生中斷的概率較低,因為上下文切換通常只在等待I/O或主動調(diào)用 Gosched() 時發(fā)生。此外,協(xié)程共享相同緩存,不易看到變量的過時版本。然而,限制程序僅使用單個處理器 (runtime.GOMAXPROCS(1)) 將失去多核性能優(yōu)勢,且無法保證未來不同調(diào)度策略下的程序健壯性。無論使用何種調(diào)度器,都必須防范競態(tài)條件。
實現(xiàn)良好并發(fā)編程的第一步是識別可能引發(fā)競爭條件的場景。在與其他協(xié)程共享資源時應格外謹慎。通過恰當?shù)耐脚c通信機制可避免多數(shù)此類問題。
Go 語言競態(tài)檢測工具
Spendy Done Stingy Done Money in bank account: 9468340
編寫并發(fā)代碼的經(jīng)驗有助于更容易地識別競態(tài)條件。核心原則是:當與其他協(xié)程共享資源,且在臨界區(qū)內(nèi)訪問這些資源未經(jīng)正確同步時,就極有可能發(fā)生競態(tài)條件。
Stingy Done Spendy Done Money in bank account: -9811630
我們可以在程序中的關鍵位置設置斷點,從而嘗試找出“吝嗇鬼”和“揮霍者”程序中存在的問題。不過,我們很可能無法發(fā)現(xiàn)問題所在,因為在中斷點處暫停程序的執(zhí)行會降低程序的運行速度,這樣一來,導致競爭條件出現(xiàn)的可能性也就降低了。
“競態(tài)條件”正是“海森堡錯誤”的典型例子。這種錯誤以物理學家維爾納·海森堡的名字命名,其名稱源于海森堡的量子力學不確定性原理。所謂“海森堡錯誤”,指的是當我們試圖調(diào)試或隔離這類錯誤時,它們會突然消失或改變其行為。由于這類錯誤極難被調(diào)試出來,因此最好的解決辦法就是從源頭上避免它們的出現(xiàn)。因此,了解導致競態(tài)條件的原因,并掌握在代碼中預防這類錯誤的方法就顯得非常重要。
讓我們通過一個具體的場景來了解一下,為什么會得到這些奇怪的結(jié)果。為了簡化問題,我們假設系統(tǒng)中只有一個處理器,因此不存在并行處理的情況。下圖展示了我們所開發(fā)的“吝嗇鬼”和“揮霍者”程序中出現(xiàn)的那種競爭條件。
在時間戳1到3期間,是Spendy在執(zhí)行操作。它從共享內(nèi)存中讀取數(shù)值100,并將其存儲在處理器的寄存器中。然后,它再從該數(shù)值中減去10,將90存回共享內(nèi)存中。在時間戳4到6期間,輪到Stingy來操作了。它讀取共享內(nèi)存中的90,然后加上10,再將100存回堆中的共享變量中。而從時間戳7到11期間,情況開始變得糟糕起來。在時間戳7時,Spendy從主內(nèi)存中讀取數(shù)值100,并將其存儲在處理器的寄存器中。

在時間戳8時,發(fā)生了上下文切換,Stingy的協(xié)程開始在處理器上執(zhí)行。它試圖從共享變量中讀取數(shù)值100,因為Stingy的線程之前還沒有機會更新該變量的值。在時間戳9和10時,這兩個協(xié)程分別對共享變量進行了加減10的操作。之后,Spendy將共享變量的值改寫為90。而在時間戳11時,Stingy的線程再次將共享變量的值改為110。最終,我們花費了20美元,但也收回了20美元,不過賬戶里還多出了10美元。
定義: “原子”這個詞源自古希臘語,意為“不可分割的”。在計算機科學中,所謂“原子操作”,指的是那種無法被中斷的操作。
出現(xiàn)這個問題是因為money += 10和money -= 10這兩個操作并非原子操作;編譯后,它們會被拆分成多條指令來執(zhí)行。在指令執(zhí)行過程中,可能會出現(xiàn)中斷情況。來自其他goroutine的指令也可能會干擾正常的執(zhí)行流程,從而引發(fā)競爭條件。當這種干擾發(fā)生時,就會導致不可預測的結(jié)果。
定義:在代碼中,所謂“臨界區(qū)”,指的是這樣一段指令:這些指令在執(zhí)行時不能受到其他指令的干擾,因為其他指令的執(zhí)行可能會影響該臨界區(qū)內(nèi)所使用的數(shù)據(jù)狀態(tài)。如果允許這種干擾發(fā)生,就可能導致競爭條件出現(xiàn)。
即便指令是原子性的,我們?nèi)钥赡苡龅絾栴}。還記得本章開頭我們討論過處理器緩存和寄存器的內(nèi)容嗎?每個處理器核心都有各自的緩存和寄存器,用于存儲那些經(jīng)常被使用的變量。在編譯代碼時,編譯器會進行優(yōu)化處理,盡量讓這些變量一直保留在CPU的寄存器或緩存中,而不會將它們強制存回內(nèi)存。這樣一來,兩個并行運行的協(xié)程就有可能同時訪問到這些變量。
在完成定期向內(nèi)存的刷新操作之前,各個獨立的CPU無法感知到其他CPU所做的更改。
當我們在并行環(huán)境中執(zhí)行那些編寫不佳的并發(fā)程序時,出現(xiàn)這類錯誤的可能性就更大。由于多個協(xié)程可以同時執(zhí)行某些操作,因此發(fā)生競爭條件的風險也會增加。在我們的“吝嗇鬼與揮霍者”程序中,兩個協(xié)程很可能會在并行運行時同時讀取“money”變量,然后再同時對其進行寫操作。
當我們使用協(xié)程(或任何用戶級線程)時,且系統(tǒng)僅在單個處理器上運行時,運行時幾乎不會在這些指令的執(zhí)行過程中進行中斷。因為用戶級調(diào)度通常是非搶占式的:它只會在某些特定情況下才進行上下文切換,比如在發(fā)生I/O操作時,或者當應用程序調(diào)用Gosched()函數(shù)時。這與操作系統(tǒng)的調(diào)度方式不同,操作系統(tǒng)通常是搶占式的,可以隨時中斷程序的執(zhí)行。此外,各個協(xié)程幾乎不可能看到變量的過時版本,因為所有協(xié)程都在同一個處理器上運行,共享相同的緩存。實際上,如果你使用runtime.GomAxPRocs(1)來查看相關數(shù)據(jù),很可能不會發(fā)現(xiàn)同樣的問題。
顯然,這不是一個理想的解決方案。首先,我們將會失去使用多處理器的優(yōu)勢;其次,也無法保證這種方法能徹底解決這個問題。Go語言的后續(xù)版本可能會采用不同的調(diào)度方式,從而導致我們的程序出錯。無論我們使用哪種調(diào)度系統(tǒng),都必須防范潛在的競態(tài)條件問題。這樣,無論程序在何種環(huán)境下運行,我們都能避免出現(xiàn)問題。
通過各種渠道進行通信,并實現(xiàn)各進程的有序執(zhí)行。這種處理并發(fā)的方式能夠有效避免許多此類錯誤。
實現(xiàn)良好的并發(fā)編程的第二步,就是能夠識別出可能發(fā)生競爭條件的情況。在與其他協(xié)程共享資源時,我們必須格外小心。一旦確定了那些容易出現(xiàn)問題的代碼片段,我們就可以采取相應的最佳實踐來確保資源能夠被安全地共享。
Go 提供了競態(tài)檢測工具,可通過 -race 編譯選項啟用。此選項會在所有內(nèi)存訪問處插入額外代碼,追蹤不同 goroutine 對內(nèi)存的讀寫順序,并在檢測到競態(tài)條件時輸出警告。對上述程序使用 -race 選項編譯運行,會得到類似以下輸出:
package main
import (
"fmt"
"time"
)
/*
Note: this program has a race condition for demonstration purposes
In later chapters we cover how to wait for threads to complete their work
*/
func stingy(money *int) {
for i := 0; i < 1000000; i++ {
*money += 10
}
fmt.Println("Stingy Done")
}
func spendy(money *int) {
for i := 0; i < 1000000; i++ {
*money -= 10
}
fmt.Println("Spendy Done")
}
func main() {
money := 100
go stingy(&money)
go spendy(&money)
time.Sleep(2 * time.Second)
fmt.Println("Money in bank account: ", money)
}
? CGO_ENABLED=1 go run -race stingyspendy.go
==================
WARNING: DATA RACE
Read at 0x00c0001a6028 by goroutine 8:
main.spendy()
/mnt/c/Users/COLORFUL/Downloads/ConcurrentProgrammingWithGo-main/chapter3/listing3.5_6/stingyspendy.go:21 +0x39
main.main.gowrap2()
/mnt/c/Users/COLORFUL/Downloads/ConcurrentProgrammingWithGo-main/chapter3/listing3.5_6/stingyspendy.go:29 +0x2e
Previous write at 0x00c0001a6028 by goroutine 7:
main.stingy()
/mnt/c/Users/COLORFUL/Downloads/ConcurrentProgrammingWithGo-main/chapter3/listing3.5_6/stingyspendy.go:14 +0x4b
main.main.gowrap1()
/mnt/c/Users/COLORFUL/Downloads/ConcurrentProgrammingWithGo-main/chapter3/listing3.5_6/stingyspendy.go:28 +0x2e
Goroutine 8 (running) created at:
main.main()
/mnt/c/Users/COLORFUL/Downloads/ConcurrentProgrammingWithGo-main/chapter3/listing3.5_6/stingyspendy.go:29 +0x10a
Goroutine 7 (running) created at:
main.main()
/mnt/c/Users/COLORFUL/Downloads/ConcurrentProgrammingWithGo-main/chapter3/listing3.5_6/stingyspendy.go:28 +0xa4
==================
==================隨著你編寫更多并發(fā)代碼的經(jīng)驗積累,識別競爭條件會變得更容易。需要記住的是:在代碼的臨界區(qū)域內(nèi),當你與其他協(xié)程共享資源(比如內(nèi)存)時,如果不對對共享資源的訪問進行同步處理,就很可能出現(xiàn)競爭條件。
總結(jié)
內(nèi)存共享是多個協(xié)程間進行通信協(xié)作的一種方式。
多處理器與多核架構(gòu)從硬件上支持了線程間的內(nèi)存共享。
當多個協(xié)程共享內(nèi)存等資源且未經(jīng)正確同步時,可能產(chǎn)生競態(tài)條件,導致非預期結(jié)果。
臨界區(qū)是指不能被其他并發(fā)流干擾的指令序列,否則可能引發(fā)競態(tài)條件。
僅在臨界區(qū)外調(diào)用調(diào)度器無法解決競態(tài)條件問題。
通過適當?shù)耐脚c通信機制可以避免競態(tài)條件。
Go 語言提供了競態(tài)檢測工具,可輔助定位代碼中的數(shù)據(jù)競爭問題。
以上就是Go語言中利用共享內(nèi)存進行線程間通信的詳細內(nèi)容,更多關于Go共享內(nèi)存進行線程間通信的資料請關注腳本之家其它相關文章!
相關文章
Go?web中cookie值安全securecookie庫使用原理
這篇文章主要為大家介紹了Go?web中cookie值安全securecookie庫使用及實現(xiàn)原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11

