Golang 入門 : 競爭條件


筆者在前文《Golang 入門 : 理解並發與並行》和《Golang 入門 : goroutine(協程)》中介紹了 Golang 對並發的原生支持以及 goroutine 的用法。本文我們來聊聊並發與並行帶來的一些副作用。

並行編程之所以難道較高,根本的原因是需要處理共享資源的同步訪問。比如在 Golang 中如果兩個或者多個 goroutine 在沒有互相同步的情況下,訪問某個共享的資源,並試圖同時讀和寫這個資源,就處於相互競爭的狀態,這種情況被稱作競爭條件(race candition)。競爭條件的存在是讓並發程序變得復雜的地方,十分容易引起潛在問題。對一個共享資源的讀和寫操作必須是原子化的,換句話說,同一時刻只能有一個 goroutine 對共享資源進行讀和寫操作。

goroutine 引入的競爭條件

讓我們來通過下面的 demo 來觀察 goroutine 引入的競爭條件,為了讓觀察結果明顯,我們采取了一些極端措施:

package main

import (
    "sync"
    "fmt"
    "runtime"
)

var(
    // counter是所有goroutine都要增加其值的變量
    counter int

    // wg用來等待程序結束
    wg sync.WaitGroup
)

// main是所有Go程序的入口
func main(){
    runtime.GOMAXPROCS(1)
    // 計數加2,表示要等待兩個goroutine
    wg.Add(2)

    // 創建兩個goroutine
    go incCounter(1)
    go incCounter(2)

    // 等待goroutine結束
    wg.Wait()
    fmt.Println("Final Counter:", counter)
}

// incCounter增加包里counter變量的值
func incCounter(id int){
    // 在函數退出時調用Done來通知main函數工作已經完成
    defer wg.Done()

    for count := 0; count < 2; count++{
        // 捕獲counter的值
        value := counter

        // 當前goroutine從線程退出,並放回到隊列
        runtime.Gosched()

        // 增加本地value變量的值
        value++

        // 將該值保存回counter
        counter = value
    }
}

運行上面的代碼,輸出結果如下:

Final Counter: 2

上面的程序中會對變量 counter 會進行 4 次讀和寫操作,每個 goroutine 執行兩次。但是,程序終止時,counter 變量的值為 2。我們可以通過下面的圖解來理解該程序的執行過程(此圖來自互聯網):

每個 goroutine 都會覆蓋另一個 goroutine 的工作。這種覆蓋發生在 goroutine 切換的時候。每個 goroutine 創造了一個 counter 變量的副本,之后就切換到另一個 goroutine。當 這個 goroutine 再次運行的時候,counter 變量的值已經改變了,但是 goroutine 並沒有更新自己的那個副本的值,而是繼續使用這個副本的值,用這個值遞增,並存回 counter 變量,結果覆蓋了另一個 goroutine 完成的工作。 下面是對程序執行過程的解釋:

// 創建兩個 goroutine
go incCounter(1)
go incCounter(2)

程序中通 go 關鍵字和 incCounter 函數創建了兩個 goroutine。在 incCounter 函數內部對變量 counter 進行了讀和寫操作,而 counter 變量是這個示例程序里的共享資源。每個 goroutine 都會先讀出這個 counter 變量的值,並把 counter 變量的副本存入一個叫作 value 的本地變量。之后 incCounter 函數對 value 變量加 1,並最終將這個新值存回到 counter 變量。incCounter 函數在對本地變量 value加 1 前調用了 runtime 包的 Gosched 函數,這個調用會將 goroutine 從當前線程退出,給其他 goroutine 運行的機會。在兩次操作中間這樣做的目的是強制調度器切換兩個 goroutine,以便讓競爭條件的效果變得更明顯。

如果不是我們通過調用 Gosched 函數讓競爭條件的效果變得明顯,那么多次運行這段程序輸出的 counter 值很可能是不一樣的,會是 2,3,4 中的一個值。這種情況下導致的問題往往非常難以定位。

和其它編程語言一樣,Golang 提供了原子函數和鎖等機制來解決同步問題。但是使用這些機制並不會使並發編程變得更簡單。接下來筆者將介紹 Golang 中提供的 channel(通道)功能,看它是如何以簡潔的方式解決同步問題的。

參考:
《Go語言實戰》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM