golang的一大特色是對併發的支持較好,golang的併發是經過goroutine來實現的。顧名思義,goroutine就是golang實現的協程。git
咱們說併發,能夠是線程的併發,也能夠是協程的併發,協程相對於線程的優勢是協程比線程更輕量級,所以併發度能夠更高。拿goroutine來講,一個go進程包含數千個goroutine同時在跑。github
golang中,每一個goroutine是獨立的,多個goroutine爲了共同完成一個任務,須要有必定的通訊機制。golang
好比說任務T由兩個協程A、B共同完成,且A、B之間存在依賴關係,協程B依賴於協程A的執行結果,也就是隻有等協程A執行完以後,協程B才能開始執行。編程
golang中協程之間的通訊是經過channel來完成的。併發
能夠把channel理解成一個管道(pipe),數據從管道的一端流進,從另外一端流出。channel的語義是,當咱們從管道中讀數據時,讀操做會一直block直到有數據流入管道;一樣的,當咱們寫數據到管道中時,寫操做一直block直到管道中的數據被讀走。ui
unbuffered channel的語義是:當咱們從管道中讀數據時,讀操做會一直block直到有數據流入管道;一樣的,當咱們寫數據到管道中時,寫操做一直block直到管道中的數據被讀走。spa
下面的case我想在程序退出以前在屏幕上輸出hello world,爲了實現這點,我使用了done這個類型爲chan bool的channel變量。線程
package main
import (
"fmt"
"time"
)
// 接收 bool 的 cahnnel
func hello(done chan bool) {
fmt.Println("hello world")
time.Sleep(4 * time.Second)
done <- true
}
func main() {
//用 make 創建一個不為 nil 的 channel
done := make(chan bool)
fmt.Println("Main going to call hello go goroutine")
go hello(done)
<- done // 管道讀操做一直block,直到 hello goroutine執行並往管道中寫數據,註釋掉此行,main goroutine會一直執行到結束,hello goroutine不會被調度
fmt.Println("Main received data")
}
複製代碼
使用make建立channel的時候,除了類型以外,還能夠指定另一個參數capacity,capacity指定了channel的buffer長度,這種channel稱之爲buffered channel,capacity默認爲0。code
相似地,buffered channel的語義也很好理解:當buffer滿了,繼續寫就會被block;當buffer空了,繼續讀就會被block。server
package main
import (
"fmt"
"time"
)
func write(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
fmt.Println("successfully wrote", i, "to ch")
}
close(ch)
}
func main() {
ch := make(chan int, 2)
go write(ch)
time.Sleep(2 * time.Second)
for v := range ch {
fmt.Println("read value", v,"from ch")
time.Sleep(2 * time.Second)
}
}
/* successfully wrote 0 to ch successfully wrote 1 to ch read value 0 from ch successfully wrote 2 to ch read value 1 from ch successfully wrote 3 to ch read value 2 from ch successfully wrote 4 to ch read value 3 from ch read value 4 from ch */
複製代碼
mutex其實是一種鎖機制,確保在任一時間點,只有一個goroutine可以進入到臨界區(critical section),進而防止競爭條件(race condition)的發生。
下面的case中,啓動1000個goroutine來讓x自增1000次,每次運行的結果可能都不必定,x會小於等於1000。 這是由於x的自增操做不是原子的,某一時刻,goroutine1讀到x的值爲10,+1以後爲11,可是尚未寫入主存,此時發生協程切換,goroutine2開始運行,goroutine2從主存讀到x依然爲10,+1以後爲11,接下來,goroutine1和goroutine2把個字結果寫回主存(無論前後順序),x的值更新爲11,出現了兩次自增操做只實現了+1的效果。(local運行)
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
x = x + 1
wg.Done()
}
func main() {
var w sync.WaitGroup
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, &m) // 這裡必定要用 address
}
w.Wait()
fmt.Println("final value of x", x)
}
複製代碼
要解決這個問題很簡單,每次執行自增操做以前先加鎖,執行完以後再釋放鎖,以此來保證自增操做的原子性。下面的case無論運行多少次,每次運行x的值都會是1000,也就是說程序的運行結果是肯定的。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock()
x = x + 1
m.Unlock()
wg.Done()
}
func main() {
var w sync.WaitGroup
var m sync.Mutex
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, &m) // 這裡必定要用 address
}
w.Wait()
fmt.Println("final value of x", x)
}
複製代碼
特別的,咱們還可使用channel來實現mutex的功能。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, m chan int) {
m <- 1
x = x + 1
<- m
wg.Done()
}
func main() {
var w sync.WaitGroup
m := make(chan int, 1)
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, m) // 這裡必定要用 address
}
w.Wait()
fmt.Println("final value of x", x)
}
複製代碼
除了channel和mutex以外,golang還提供了WaitGroup和Select來實現併發。
WaitGroup本質上是一個counter,只有counter=1的時候纔會繼續下一步。通常咱們使用WaitGroup來實現語義:當一組goroutine都執行完成的時候,才繼續下一步。
在下面的case中,執行一個goroutine以前counter先加1,goroutine執行完退出以前,counter減1,這就保證了只有在全部的goroutine都完成以後,纔會繼續執行main goroutine。
package main
import (
"fmt"
"sync"
"time"
)
func process(i int, wg *sync.WaitGroup) {
fmt.Println("started Goroutine ", i)
time.Sleep(2 * time.Second)
fmt.Printf("Goroutine %d ended\n", i)
wg.Done() // -1
}
func main() {
no := 3
var wg sync.WaitGroup
for i := 0; i < no; i++ {
wg.Add(1) // + 1
go process(i, &wg) // wg 必定要用 pointer,不然每一個 goroutine 都會有各自的 WaitGroup
}
wg.Wait() // =0 才繼續下一步
fmt.Println("All go routines finished executing")
}
複製代碼
select的語法跟switch的語法很是相似,用來實現以下語義:當一組協程中的全部協程都處於block時則block,當這組協程中有一個協程ready時,選擇這個協程執行,當一組協程裏面有多個協程ready時,隨機選一個執行。
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
time.Sleep(6 * time.Second)
ch <- "from server1"
}
func server2(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
// 等待到其中一個 channel 回來,就執行,若是都有就會隨機
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
複製代碼
本篇介紹了golang中關於併發編程的幾個關鍵概念。golang的好處是從golang出發能夠很清楚搞清楚併發中的不少關鍵概念。
下篇介紹併發編程中的關鍵概念,它們之間的聯繫,以及這些關鍵概念在golang中的實現。