一、前言
編寫正確的程序本身就不容易,編寫正確的並發程序更是難中之難,那麽並發編程究竟難道哪裏那?本節我們就來一探究竟。
二、數據競爭的存在
當兩個或者多個線程(goroutine)在沒有任何同步措施的情況下同時讀寫同一個共享資源時候,這多個線程(goroutine)就處于數據競爭狀態,數據競爭會導致程序的運行結果超出寫代碼的人的期望。下面我們來看個例子:
package mainimport ( “fmt”)var a int//goroutine1func main() { //1,gouroutine2 go func(){ a = 1//1.1 }() //2 if 0 == a{//2.1 fmt.Println(a)//2.2 }}
- 如上代碼首先創建了一個int類型的變量,默認被初始化爲0值,運行main函數會啓動一個進程和這個進程中的一個運行main函數的goroutine(輕量級線程)
- 在main函數內使用go語句創建了一個新的goroutine(該goroutine運行匿名函數裏面的內容)並啓動運行,匿名函數內給變量賦值爲1
- main函數裏面代碼2判斷如果變量a的值爲0,則打印a的值。
運行main函數後,啓動的進程裏面存在兩個並發運行的線程,分別是開啓的新goroutine(起名爲goroutine2)和main函數所在的goroutine(起名爲goroutine1),前者試圖修改共享變量a,後者試圖讀取共享變量a,也就是存在兩個線程在沒有任何同步的情況下對同一個共享變量進行讀寫訪問,這就出現了數據競爭,由于數據競爭存在,導致上面程序可能會有下面三種輸出:
- 輸出0,由于運行時調度系統的隨機性,會存在goroutine1的2.2代碼比goroutine2的代碼1.1先執行
- 輸出1,當存在goroutine1先執行代碼2.1,然後goroutine2在執行代碼1.1,最後goroutine1在執行代碼2.2的時候
- 什麽都不輸出,當goroutine2執行先于goroutine1的2.1代碼時候。
由于數據競爭的存在上面一段很短的代碼會有三種可能的輸出,究其原因是goroutine1和groutine2的運行時序是不確定的,也就是沒有對他們的操作做同步,以便讓這些內存操作變爲可以預知的順序執行。
這裏編寫程序者或許受單線程模型的影響認爲代碼1.1會先于代碼2.1執行,當發現輸出不符合預期時候,或許會在代碼2.1前面讓goroutine1 休眠一會確保goroutine2執行完畢1.1後在讓goroutine1執行2.1,這看起來或許有效,但是這是非常低效,並且並不是所有情況下都可以解決的。
正確的做法可以使用信號量等同步措施,保證goroutine2執行完畢再讓goroutine1執行代碼2.1,如下面代碼,我們使用sync包的WaitGroup來保證goroutine2執行完畢代碼2.1後,goroutine1才可以執行步驟4.1,關于WaitGroup後面章節我們具體會講解:
package mainimport ( “fmt” “sync”)var a intvar wg sync.WaitGroup//信號量//goroutine1func main() { //1. wg.Add(1);//一個信號 //2. goroutine1 go func(){ a = 1//2.1 wg.Done() }() wg.Wait()//3. 等待goroutine1運行結束 //4 if 0 == a{//4.1 fmt.Println(a)//4.2 }}三、操作的原子性
所謂原子性操作是指當執行一系列操作時候,這些操作那麽全部被執行,那麽全部不被執行,不存在只執行其中一部分的情況。在設計計數器時候一般都是先讀取當前值,然後+1,然後更新,這個過程是讀-改-寫的過程,如果不能保證這個過程是原子性,那麽就會出現線程安全問題。如下代碼是線程不安全的,因爲不能保證a++是原子性操作:
package mainimport ( “fmt” “sync”)var count int32var wg sync.WaitGroup //信號量const THREAD_NUM = 1000//goroutine1func main() { //1.信號 wg.Add(THREAD_NUM) //2. goroutine for i := 0; i < THREAD_NUM; i++ { go func() { count++//2.1 wg.Done()//2.2 }() } wg.Wait() //3. 等待goroutine運行結束 fmt.Println(count) //4輸出計數}
- 如上代碼在main函數所在爲goroutine內創建了THREAD_NUM個goroutine,每個新的goroutine執行代碼2.1對變量count計數增加1。
- 這裏創建了THREADNUM個信號量,用來在代碼3處等待THREADNUM個goroutine執行完畢,然後輸出最終計數,執行上面代碼我們 期望輸出1000,但是實際卻不是。
這是因爲a++操作本身不是原子性的,其等價于b := count;b=b+1;count=b;是三步操作,所以可能導致導致計數不准確,如下表: