Golang內存模型(官方文檔中英文翻譯)

The Go Memory Model

Golang內存模型(官方文檔中英文翻譯)

點擊:更多詳情請前往博客地址 點擊:或者git地址 點擊: Go Memory Model 示例代碼

Version of May 31, 2014

Introduction Advice Happens Before Synchronization Initialization Goroutine creation Goroutine destruction Channel communication Locks Once Incorrect synchronization

  1. 介紹
  2. 建議
  3. Happens Before原則
  4. 同步
  5. 初始化
  6. Goroutine創建
  7. Goroutine銷燬
  8. 通道通信
  9. 一次
  10. 不正確的同步

Introduction

介紹

The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to observe values produced by writes to the same variable in a different goroutine.

Go內存模型指出一個goroutine在讀取一個變量時候,能夠觀察到其他線程對相同變量寫的結果值。

Advice

建議

Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.

To serialize access, protect the data with channel operations or other synchronization primitives such as those in the sync and sync/atomic packages.

If you must read the rest of this document to understand the behavior of your program, you are being too clever.

Don't be clever.

程序中多個goroutine同時修改相同數據,必須使之能夠序列化訪問。 爲了序列化訪問:保護數據安全可以使用channel操作或者其他同步原語例如sync及sync/atomic包。 如果你必須閱讀本文檔的其餘部分來理解您的程序的行爲,這樣將是很聰明的做法。當然也不要太聰明。

Happens Before

Happens Before原則

Within a single goroutine, reads and writes must behave as if they executed in the order specified by the program. That is, compilers and processors may reorder the reads and writes executed within a single goroutine only when the reordering does not change the behavior within that goroutine as defined by the language specification. Because of this reordering, the execution order observed by one goroutine may differ from the order perceived by another. For example, if one goroutine executes a = 1; b = 2;, another might observe the updated value of b before the updated value of a.

在單個goroutine,讀寫必須表現得好像他們指定的程序的順序執行一致。 也就是說,編譯器和處理器可能會對讀寫進行重新排序,在一個執行goroutine中, 只有當重新排序並不會改變goroutine在語言規範定義行爲時,纔會進行重排序。 因爲重新排序之後,執行順序在一個goroutine可能不同於另一個goroutine中觀察會保持一致。 舉個例子,如果一個goroutine執行a = 1,b = 2; 另一個可以觀察b的值更新,在a的值更新之前。

To specify the requirements of reads and writes, we define happens before, a partial order on the execution of memory operations in a Go program. If event e1 happens before event e2, then we say that e2 happens after e1. Also, if e1 does not happen before e2 and does not happen after e2, then we say that e1 and e2 happen concurrently.

爲了詳細說明Go程序的讀寫在內存中的偏序關係,我們定義了happen-before原則。 如果時間e1發生在事件e2之前,那麼則e2發生在e1之後。 如果e1既不發生在e2之前也不發生在它之後,那麼則說明e1和e2同時發生。

Within a single goroutine, the happens-before order is the order expressed by the program.

在一個單goroutine中,happen-before的順序與程序中定義的順序保持一致。

A read r of a variable v is allowed to observe a write w to v if both of the following hold: 1、r does not happen before w. 2、There is no other write w' to v that happens after w but before r.

如果滿足下述兩個規則:那麼對變量v的讀操作r能夠觀察到對變量v的寫操作w:

  1. r沒有發生在w之前
  2. 沒有其他針對變量v的寫操作發生在w操作之後,r之前

To guarantee that a read r of a variable v observes a particular write w to v, ensure that w is the only write r is allowed to observe. That is, r is guaranteed to observe w if both of the following hold: 1、w happens before r. 2、Any other write to the shared variable v either happens before w or after r.

爲了保證對變量v的讀操作能夠觀察到對變量v的特定的寫操作w的值,確保w是唯一的寫操作,r的變化允許被觀察到。 那麼,如果滿足下述兩個原則,r將能夠觀察到w

  1. w發生在r之前
  2. 任何對共享變量v的寫操作發生在寫操作w之前或者讀操作r之後

This pair of conditions is stronger than the first pair; it requires that there are no other writes happening concurrently with w or r.

這對條件強於第一條;它需要沒有其他任何的寫操作與讀操作w及寫操作r同時發生。

Within a single goroutine, there is no concurrency, so the two definitions are equivalent: a read r observes the value written by the most recent write w to v. When multiple goroutines access a shared variable v, they must use synchronization events to establish happens-before conditions that ensure reads observe the desired writes.

在單一的goroutine中,由於不可能同時發生,因此兩個定義也是等價的: 針對變量v的r操作能夠觀察到最近對改變了的寫操作 當多個goroutine同時訪問一個共享變量v時,必須使用sync同步事件建立happen-before條件來確保讀操作觀察到期望的寫操作的變化。

The initialization of variable v with the zero value for v's type behaves as a write in the memory model.

變量v的初始化以零值作爲默認值的類型的行爲作爲一個寫在內存模型。

Reads and writes of values larger than a single machine word behave as multiple machine-word-sized operations in an unspecified order.

如果讀取和寫入的值大於一個機器字,那麼多個多機器字節變量的操作發生在一個未指明的順序。

Synchronization

同步

Initialization

初始化

Program initialization runs in a single goroutine, but that goroutine may create other goroutines, which run concurrently.

程序初始化運行在單個goroutine上,但是goroutine可能創建其他goroutine,併發運行。

If a package p imports package q, the completion of q's init functions happens before the start of any of p's.

如果一個包p導入了一個包q,q的初始化函數的結束先於任何p的開始完成。

The start of the function main.main happens after all init functions have finished.

函數main.mian在所有init初始化函數的完成後發生。

Goroutine creation

Goroutine創建

The go statement that starts a new goroutine happens before the goroutine's execution begins.

用於啓動goroutine的go語句在goroutine之前運行。

For example, in this program:

var a string

func f() {
	print(a)
}

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

calling hello will print "hello, world" at some point in the future (perhaps after hello has returned).

調用hello函數,會在某個時刻打印“hello, world”(有可能是在hello函數返回之後)。

Goroutine destruction

Goroutine銷燬

The exit of a goroutine is not guaranteed to happen before any event in the program.

goroutine的退出不保證一定會先於其他事件發生。

For example, in this program:

var a string

func hello() {
	go func() { a = "hello" }()
	print(a) // 由於沒有使用同步相關事件,因此有可能a的賦值對其他線程是不可見的
}

the assignment to a is not followed by any synchronization event, so it is not guaranteed to be observed by any other goroutine. In fact, an aggressive compiler might delete the entire go statement.

對a的分配賦值沒有跟隨任何同步事件之後, 所以不能保證會被其他goroutine觀察到。 事實上,一個優秀的編譯器可能會刪除整個go聲明語句。

If the effects of a goroutine must be observed by another goroutine, use a synchronization mechanism such as a lock or channel communication to establish a relative ordering.

如果想讓一個goroutine的執行操作影響必須對另外一個goroutine可見, 可以使用同步機制例如lock或者channel通信來確定相關順序。

Channel communication

通道通信

Channel communication is the main method of synchronization between goroutines. Each send on a particular channel is matched to a corresponding receive from that channel, usually in a different goroutine.

通道通信是主要的方法來解決多個goroutine之間的同步。 每發送一個特定的信道匹配相應的接收通道,通常在不同的goroutine中。

A send on a channel happens before the corresponding receive from that channel completes.

一個通道的發送發生與相應的通道接收之前完成

This program:

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

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

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

is guaranteed to print "hello, world". The write to a happens before the send on c, which happens before the corresponding receive on c completes, which happens before the print.

確保一定能夠打印出“hello,world”。 對a的寫先於對c通道信息的發送,c通道的發送一定會先於c通道接受的完成,因此這些都先於print完成。

The closing of a channel happens before a receive that returns a zero value because the channel is closed.

關閉一個通道發生之前收到返回零值 因爲通道是關閉的

In the previous example, replacing c <- 0 with close(c) yields a program with the same guaranteed behavior.

在上一個示例中,用close(c)取代c < - 0,同樣能夠保證相同的行爲

A receive from an unbuffered channel happens before the send on that channel completes.

從無緩衝channel中接收操作 發生在該channel的發送操作結束前

This program (as above, but with the send and receive statements swapped and using an unbuffered channel):

var c = make(chan int)
var a string

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

is also guaranteed to print "hello, world". The write to a happens before the receive on c, which happens before the corresponding send on c completes, which happens before the print.

任然能夠保證一定會輸出“hello,world” 對a的寫操作先於通道c的信號接收,這也發生在相應的發送完成之前,當然也先於print的發生。

If the channel were buffered (e.g., c = make(chan int, 1)) then the program would not be guaranteed to print "hello, world". (It might print the empty string, crash, or do something else.)

如果是緩存通道,那麼程序則不一定能保證會打印“hello,world”。(可能打印空字符串或者crash、或者其他未知情況)

The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes.

kth收到一個通道先於k+Cth發送通道完成.

This rule generalizes the previous rule to buffered channels. It allows a counting semaphore to be modeled by a buffered channel: the number of items in the channel corresponds to the number of active uses, the capacity of the channel corresponds to the maximum number of simultaneous uses, sending an item acquires the semaphore, and receiving an item releases the semaphore. This is a common idiom for limiting concurrency.

這些定義了緩存channel通道前序規則。 允許一個計數信號量通過緩存通道來模擬: 通道緩存數量與活躍用戶保存一致,通道的最大容量與併發同時執行的最大人數保持一致,發送一個item獲取信號量及接受一個item釋放信號量。這個一個慣用的方式來限制併發。

This program starts a goroutine for every entry in the work list, but the goroutines coordinate using the limit channel to ensure that at most three are running work functions at a time.

示例:程序爲工作列表中的每一個條目啓動一個goroutine,但是各個goroutine之間使用受限制的緩存通道來確保最多隻能有三個運行work在同時執行。

var limit = make(chan int, 3)

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

Locks

The sync package implements two lock data types, sync.Mutex and sync.RWMutex.

sync包實現了兩種類型的鎖:互斥鎖及讀寫鎖

For any sync.Mutex or sync.RWMutex variable l and n < m, call n of l.Unlock() happens before call m of l.Lock() returns.

對於任意 sync.Mutex 或 sync.RWMutex 變量l。 如果 n < m ,那麼第n次 l.Unlock() 調用在第 m次 l.Lock()調用返回前發生。

This program:

// 對同一個變量l的解鎖n次和加鎖操作m次,如果n<m,即解鎖次數1小於加鎖次數2,因此解鎖操作先於加鎖操作完成
// 即f函數的unlock先於第二個lock完成,而第二個lock又先於print完成,因此保證a1的賦值能被打印出來
var l sync.Mutex
var a string

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

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

is guaranteed to print "hello, world". The first call to l.Unlock() (in f) happens before the second call to l.Lock() (in main) returns, which happens before the print.

可以確保輸出“hello, world”結果。因爲,第一次 l.Unlock() 調用(在f函數中)在第二次 l.Lock() 調用(在main 函數中)返回之前發生, 也就是在 print 函數調用之前發生。

For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after call n to l.Unlock and the matching l.RUnlock happens before call n+1 to l.Lock.

對於任何調用變量l的RLock,第n次調用l.RLock必定發生在第n次l.Unlock之後, 與之相對應的l.RUnlock必定發生在第n+1次l.Lock之前。

Once

一次執行

The sync package provides a safe mechanism for initialization in the presence of multiple goroutines through the use of the Once type. Multiple threads can execute once.Do(f) for a particular f, but only one will run f(), and the other calls block until f() has returned.

sync包提供了一個安全的機制保證多個goroutine的初始化只被執行一次。多個線程都會調用執行once.Do(f)函數,但是f函數只會執行一次。

A single call of f() from once.Do(f) happens (returns) before any call of once.Do(f) returns.

通過once.Do(f)單次調用f()的返回先於其他調用once.Do(f)的返回。

In this program:

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

calling twoprint causes "hello, world" to be printed twice. The first call to doprint runs setup once.

twoprint函數將會兩次打印輸出,通過once.Do(setup)調用的setup只會有一次輸出。

Incorrect synchronization

錯誤的同步

Note that a read r may observe the value written by a write w that happens concurrently with r. Even if this occurs,it does not imply that reads happening after r will observe writes that happened before w.

[錯誤認爲:] 如果讀操作和寫操作同時發生,讀操作r可以觀察到寫操作w的變化。 即使發生這種情況,它也並不意味着即使讀操作發生在寫操作r之後就能觀察到寫入值writes就發生在w之前。

In this program:

var a, b int

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

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

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

it can happen that g prints 2 and then 0.

有可能打印出2和0(即b已經賦值爲2,但是a還未被賦值)

This fact invalidates a few common idioms.

下述示例爲一些無效錯誤的慣例。

Double-checked locking is an attempt to avoid the overhead of synchronization. For example, the twoprint program might be incorrectly written as:

雙檢鎖是爲了避免同步的開銷,例如,twoprint程序可能會錯誤地寫成:

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

but there is no guarantee that, in doprint, observing the write to done implies observing the write to a. This version can (incorrectly) print an empty string instead of "hello, world".

這裏不能保證,在doprint函數裏面,能夠觀察到變量done的值,就暗示着能夠觀察到對a的賦值變化。 這裏可能會錯誤的打印空,而不是“hello, world”。

Another incorrect idiom is busy waiting for a value, as in:

下述爲另外一個錯誤示例(一直忙等)

var a string
var done bool

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

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

As before, there is no guarantee that, in main, observing the write to done implies observing the write to a, so this program could print an empty string too. Worse, there is no guarantee that the write to done will ever be observed by main, since there are no synchronization events between the two threads. The loop in main is not guaranteed to finish.

上述示例所示,在主函數main中,觀察到done值爲true並不能保證一定能看到對a的賦值變化, 因此本示例程序也有可能打印空值。 更糟糕的是,setup函數對done=true的賦值,並不一定能被main函數觀察到,因此這兩個線程之間並沒有使用任何的同步事件。 因此for循環在主函數中並不能保證一定會循環結束。

There are subtler variants on this theme, such as this program.

下述爲一些更爲難以發現的變體(錯誤示例)

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

Even if main observes g != nil and exits its loop, there is no guarantee that it will observe the initialized value for g.msg.

即使主函數觀察到g!=null且退出了循環,也不能保證main能夠觀察到g.msg的賦值內容。

In all these examples, the solution is the same: use explicit synchronization.

對於所有的示例,都可以採用同樣的解決辦法:使用明確的同步。

go-faq

Q: When should we use Go channels as opposed to sync.Mutex?

Q: 什麼時候我們應該使用Go channels而不是sync.Mutex? A: There is no clear answer to this question. If there's information that multiple goroutines need to use, and those goroutines don't specifically interact with each other, I tend to store the information in a shared data structure protected by locks. I use channels for producer-consumer interactions between goroutines, and when one goroutine needs to wait for another goroutine to do something.

A: 沒有明確的答案回答這個問題。如果有多個了goroutine需要使用共同的信息, 且這些goroutine之間沒有相互作用,我傾向於使用locks來保護這些共享數據結構。 通常使用channel來實現生產者及消費者之間的信息交互,以及適用於一個goroutine 需要等待另外一個goroutine去完成某些操作。 Q: Why does 6.824 use Go?

爲什麼6.824使用Go? A: Go is a good fit for distibuted systems programming: it supports concurrency well, it has a convenient RPC library, and it is garbage-collected. There are other languages that might work as well for 6.824, but Go was particularly fashionable when we were creating this set of labs. 6.824 used C++ before Go; C++ worked pretty well, but its lack of garbage collection made threaded code particularly bug-prone.

A: Go非常適合分佈式系統開發:對併發支持良好,擁有高效的RPC庫,具有垃圾回收機制。 當然也可能有其他語言適合於6.824,但是Go語言比較流行。 6.824之前使用c++語言,c++表現的更好,但是缺少自動垃圾回收是的多線程編程更易於出現bug。 Q: How can I wait for goroutines to finish?

如何等待goroutine執行完畢? A: Try sync.WaitGroup. Or you could create a channel, have each goroutine send something on the channel when it finishes, and have the main goroutine wait for the appropriate number of channel messages.

A:使用sync.WaitGroup。或者創建一個通道,當goroutine完成時候,發送完成信號給該通道, main主線程等待合適數量的channel信息。 Q: Why is map not thread-safe?

Q:爲什麼map不是線程安全的? A: That is a bit of surprise to everyone who starts using Go. My guess is the designers didn't want programs to pay for locking for a map that isn't shared by multiple goroutines.

A:開始接觸go的人對此會感到有點奇怪。我的猜想是設置者並不希望程序爲了map而花費較大開銷, 因此map不能用於多線程之間共享。 Q: Why does Go have pointers, instead of just object references?

爲什麼Go擁有指針,而不是僅僅支持對象引用? A: The designers of Go intended it to be used for systems programming, where the programmer often wants control of how things are stored and passed around. For example, Java passes integers, references, etc. by value, which means that a callee cannot change the caller's value of the integer, reference, etc. In Go, a caller can pass a pointer to a variable to the callee, and the callee can use the pointer to modify the caller's variable.

A:Go的設計者傾向於go爲系統編程,程序經常需要控制存儲及傳遞對象。 例如:java傳遞integers、引用等等。如果通過值傳遞方式,意味着被調用者不能改變調用者傳過來的(integer, reference等等)值。 在Go語言中,調用者能夠傳遞一個變量的指針給被調用者,這樣被調用者能夠修改調用者傳遞過來的值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章