這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
- 初始化
程式的初始化在一個獨立的goroutine中執行。在初始化過程中建立的goroutine將在 第一個用於初始化goroutine執行完成後啟動。
如果包p匯入了包q,包q的init初始化函數將在包p的初始化之前執行。
程式的入口函數 main.main 則是在所有的 init 函數執行完成 之後啟動。
在任意init函數中新建立的goroutines,將在所有的init 函數完成後執行。
goroutine的建立
用於啟動goroutine的go語句在goroutine之前運行。
例如,下面的程式:
var a stringfunc f() { print(a)}func hello() { a = "hello, world" go f()}
調用hello函數,會在某個時刻列印“hello, world”(有可能是在hello函數返回之後)。
Channel communication 管道通訊
用管道通訊是兩個goroutines之間同步的主要方法。在管道上執行的發送操作會關聯到該管道的 接收操作,這通常對應goroutines。
管道上的發送操作發生在管道的接收完成之前(happens before)。
例如這個程式:
var c = make(chan int, 10)var a stringfunc f() { a = "hello, world" c <- 0}func main() { go f() <-c print(a)}
可以確保會輸出"hello, world"。因為,a的賦值發生在向管道 c發送資料之前,而管道的發送操作在管道接收完成之前發生。 因此,在print 的時候,a已經被賦值。
從一個unbuffered管道接收資料在向管道發送資料完成之前發送。下面的是樣本程式:
package mainvar c = make(chan int)var a stringfunc f() { a = "hello, world" <-c}func main() { go f() c <- 0 print(a)}
同樣可以確保輸出“hello, world”。因為,a的賦值在從管道接收資料 前發生,而從管道接收資料操作在向unbuffered 管道發送完成之前發生。所以,在print 的時候,a已經被賦值。
如果用的是緩衝管道(如 c = make(chan int, 1) ),將不能保證輸出 “hello, world”結果(可能會是Null 字元串, 但肯定不會是他未知的字串, 或導致程式崩潰)。
鎖
包sync實現了兩種類型的鎖: sync.Mutex 和 sync.RWMutex。
對於任意 sync.Mutex 或 sync.RWMutex 變數l。 如果 n < m ,那麼第n次 l.Unlock() 調用在第 m次 l.Lock() 調用返回前發生。
例如程式:
var l sync.Mutexvar a stringfunc f() { a = "hello, world" l.Unlock()}func main() { l.Lock() go f() l.Lock() print(a)}
可以確保輸出“hello, world”結果。因為,第一次 l.Unlock() 調用(在f函數中)在第二次 l.Lock() 調用 (在main 函數中)返回之前發生,也就是在 print 函數調用之前發生。
對於任何呼叫到一個sync.RWMutex變數l l.RLock,有一個n使得l.RLock第n個呼叫l.Unlock後發生(返回)和n +1'th之前的匹配l.RUnlock發生調用l.Lock。
9.3.5. once
包sync提供了一個在多個goroutines中進行初始化的方法。多個goroutines可以 通過 once.Do(f) 方式調用f函數。 但是,f函數 只會被執行一次,其他的調用將被阻塞直到唯一執行的f()返回。
once.Do(f) 中唯一執行的f()發生在所有的 once.Do(f) 返回之前。
有代碼:
var once sync.Oncevar a stringfunc setup() { a = "hello, world"}func doprint() { once.Do(setup) print(a)}func twoprint() { go doprint() go doprint()}
調用twoprint會輸出“hello, world”兩次。第一次twoprint 函數會運行setup唯一一次。
錯誤的同步方式
注意:變數讀操作雖然可以偵測到變數的寫操作,但是並不能保證對變數的讀操作就一定發生在寫操作之後。
例如:
package mainvar a, b intfunc f() { a = 1 b = 2}func g() { print(b) print(a)}func main() { go f() g()}
函數g可能輸出2,也可能輸出0。
這種情形使得我們必須迴避一些看似合理的用法。
這裡用重複資料偵測的方法來代替同步。在例子中,twoprint函數可能得到錯誤的值:
package mainimport ( "sync" "time")var once sync.Oncevar a stringvar done boolfunc setup() { a = "hello, world" done = true}func doprint() { if !done { once.Do(setup) } print(a)}func twoprint() { go doprint() go doprint()}func main() { twoprint() time.Sleep(8000)}
在doprint函數中,寫done暗示已經給a賦值了。 但是沒有辦法給出保證,函數可能輸出空的值(在2個goroutines中同時執行到測試語句)。
另一個錯誤陷阱是忙等待:
package mainvar a stringvar done boolfunc setup() { a = "hello, world" done = true}func main() { go setup() for !done { } print(a)}
我們沒有辦法保證在main中看到了done值被修改的同時也 能看到a被修改,因此程式可能輸出Null 字元串。 更壞的結果是,main 函數可能永遠不知道done被修改,因為在兩個線程之間沒有同步操作,這樣main 函數永遠不能返回。
下面的用法本質上也是同樣的問題.
package maintype T struct { msg string}var g *Tfunc setup() { t := new(T) t.msg = "hello, world" g = t}func main() { go setup() for g == nil { } print(g.msg)}
即使main觀察到了 g != nil 條件並且退出了迴圈,但是任何然 不能保證它看到了g.msg的初始化之後的結果。
在這些例子中,只有一種解決方案:用顯示的同步。