這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
在上一篇文章裡,我們學習了goroutine和channel結合起來實現並發的方式。今天我們來學習另一種並發的方式。goroutine + share variables.
同時,我們還可以來比較一下goroutine和線程的不同。
share variables就是指多個goroutine同時操作的變數。而同時操作就會帶來競態(race condition)問題。
競態問題
如果你對並發有一點點瞭解,就可以跳過這一節。
競態問題就是當兩個或多個線程(goroutine)同時操作一個變數,你就無法預測他到底發生了什麼
func Deposit(amount int){ balance = balance + amount}func main() { for i := 2000; i > 0; i-- { go Deposit(10) } time.Sleep(2*time.Second) fmt.Println(balance)}
上面的代碼中,我們假設有一個銀行賬戶,初始時裡面沒有錢。然後有2000個人一起往裡面存錢,每人存10塊錢,所以結果應該是20000元。
但是結果卻不到20000。原因是
balance = balance + amount
並不是一個原子操作。你可以將其分為三步:讀取balance和amount,兩數字相加,相加結果寫回balance。現在我們假設現在賬戶內有1000元,兩個使用者(go routine)完成了第一步,拿到balance為10000,使用者a執行加10後寫回,賬戶剩餘10010,這時候使用者b也寫回balance,寫回的值依舊是10010,於是,使用者a的操作被覆蓋了。
解決競態的方法是加鎖
var ( mu sync.Mutex balance int)func Deposit(amount int){ mu.Lock() balance = balance + amount mu.Unlock()}func main() { for i := 2000; i > 0; i-- { go Deposit(10) } time.Sleep(2*time.Second) fmt.Println(balance)}
goroutine與線程的不同
第一,線程通常都有一個固定大小的棧記憶體(2mb),如果你的程式需要同時跑1000個線程,你最好準備好2G以上的記憶體。另外,如果線程的棧記憶體比較小,可能會stackoverflow,而如果棧太大,機器可能又跑不了幾個線程了。而goroutine的棧記憶體不是固定的,初始只有2kb,最大可以到1G
我們先看看node的線程,調用不到20000層就溢出了
const test = (a) => { if (a == 0){ return 0 } return test(a-1)}test(20000) //maximum call size exceeded
再來看看goroutine,可以跑到20000000層,多了1000倍不止
func test(a int) int{ if a == 0{ return a } return test(a-1)}func main() { test(20000000)}
第二,線程是核心調度的,機器上的定時器每過一個固定時間就會調度一次。核心調度的過程是很慢的,需要把線程當前的狀態存進記憶體,恢複另一個線程的狀態,更新調度器狀態。而goroutine的調度器是go runtime提供的,所以不需要切換核心的context,所以更輕量。
第三,我們可以通過設定GOMAXPROCS環境變數來控制go程式使用的線程數(預設為cpu核心數)
第四,線程往往有一個標識(一個int或者指標),而goroutine沒有標識。這也導致golang中沒有辦法儲存和擷取指定goroutine的"狀態"。官方的解釋是"this is by design, since thread-local storage tends to be abused"。換句話說,golang鼓勵你寫出更純的代碼,函數的返回不應該受到之前狀態的影響。