這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本文我們來學習一下golang裡面的並發以及golang的鎖機制。
首先我們假設這樣一個情境,有兩個協程,同時在讀寫一個數字,分別對其進行遞增和遞減,代碼如下。
func main(){ a := 1 p_a := &a go func(p_a *int){ for i := 1; i < 100000; i++ { *p_a += 1 fmt.Println(*p_a) } }(p_a) go func(p_a *int){ for i := 1; i < 100000; i++ { *p_a -= 1 fmt.Println(*p_a) } }(p_a)}
運行這段代碼我們發現什麼都沒有輸出,原因很簡單,主程式在建立完兩個協程之後就退出了。要想讓主程式不退出可以使用sync.WaitGroup
func main(){ a := 1; p_a := &a; var wg sync.WaitGroup wg.Add(2) go func(p_a *int){ for i := 1; i < 100000; i++ { *p_a += 1 } wg.Done() }(p_a) go func(p_a *int){ for i := 1; i < 100000; i++ { *p_a -= 1 } wg.Done() }(p_a) wg.Wait() fmt.Println(*p_a)}
然而,程式跑出來的結果並不是1,而且每次跑出來的都不一樣。其實,寫過一點並發的同學都知道原因。假設當前變數值為1,協程a試圖對其加1,於是協程a首先對其進行讀操作,得到1,並加1得到2,但是在2這個值被寫回變數之前,協程b讀取了變數,而此時變數依舊還是1,於是b拿到這個1並減一得到0。這時候,無論協程1先完成寫回還是協程b先完成寫回,我們得到的結果都是錯的。
要想得到正確的結果,一個最常用的方法就是加鎖。在golang裡面,我們可以使用sync.Mutex
var l sync.Mutexfunc main(){ a := 1; p_a := &a; var wg sync.WaitGroup wg.Add(2) go func(p_a *int){ for i := 1; i < 100000; i++ { l.Lock() *p_a += 1 l.Unlock() } wg.Done() }(p_a) go func(p_a *int){ for i := 1; i < 100000; i++ { l.Lock() *p_a -= 1 l.Unlock() } wg.Done() }(p_a) wg.Wait() fmt.Println(*p_a)}
golang裡面的map
當一個協程在寫map的時候,其他協程如果試圖去讀或者寫這個map,程式都會直接crash!!!很多人都在吐槽這個設計,但是我覺得,這種設計至少給了程式員立即發現問題的機會。加鎖吧少年。
死結,活鎖與饑餓
死結可以理解為完成一項任務的資源被兩個(或多個)不同的協程分別佔用了,導致它們全都處於等待狀態不能完成下去。
活鎖的例子是兩個或多個協程在執行時分別佔用了部分資源導致無法執行。於是他們都釋放資源並重新請求,可是依舊碰撞,導致仍然無法執行。
饑餓的例子是如果事務T1封鎖了資料R,事務T2又請求封鎖R,於是T2等待。T3也請求封鎖R,當T1釋放了R上的封鎖後,系統首先批准了T3的請求,T2仍然等待。然後T4又請求封鎖R,當T3釋放了R上的封鎖之後,系統又批准了T4的請求......T2可能永遠等待。