Goroutine
Go在語言層面對并發編程提供支持,采用輕量級線程(協程)實現。只需要在函數調用語句前添加go關鍵字,就可以創建并發執行單元。開發人員無需了解任何執行細節,調度器會自動將其安排到合適的系統線程上執行。goroutine是一種非常輕量級的實現,可在單個進程里執行成千上萬的并發任務。事實上,入口函數main就以goroutine運行。另有與之配套的channel類型,用以實現“以通訊來共享內存”的CSP模式。
go func() {
println("Hello, World!")
}
調度器不能保證多個goroutine執行次序,且進程退出時不會等待它們結束。默認情況下,進程啟動后僅允許一個系統線程服務于goroutine。可使用環境變量或標準函數runtime.GOMAXPROCS修改(Go 1.5默認方式)。讓高度器用多個線程實現多核并行,而不僅僅是并發。
func sum(id int) {
var x int64
for i := 0; i < math.MaxUint32; i++ {
x += int64(i)
}
println(id, x)
}
func main() {
wg := new(sync.WaitGroup)
wg.Add(2)
for i := 0; i < 2; i++ {
go func(id int) {
defer wg.Done()
sum(id)
}(i)
}
wg.Wait()
}
輸出:
$ go build -o test
$ time -p ./test
0 9223372030412324865
1 9223372030412324865
real 7.70 // 程序開始到結束時間差 (非非 CPU 時間)
user 7.66 // 用用戶態所使用用 CPU 時間片片 (多核累加)
sys 0.01 // 內核態所使用用 CPU 時間片片
$ GOMAXPROCS=2 time -p ./test
0 9223372030412324865
1 9223372030412324865
real 4.18
user 7.61// 雖然總時間差不多,但由 2 個核并行行,real 時間自自然少了許多。
sys 0.02
調用 runtime.Goexit 將立即終止當前 goroutine 執行,調度器確保所有已注冊 defer延遲調用被執行。
func main() {
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
defer wg.Done()
defer println("A.defer")
func() {
defer println("B.defer")
runtime.Goexit() // 終止止當前 goroutine
println("B") // 不會執行行
}()
println("A") // 不會執行行
}()
wg.Wait()
}
//輸出:
B.defer
A.defer
和協程 yield 作用類似,Gosched 讓出底層線程,將當前 goroutine 暫停,放回隊列等待下次被調度執行。
func main() {
wg := new(sync.WaitGroup)
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 6; i++ {
println(i)
if i == 3 { runtime.Gosched() }
}
}()
go func() {
defer wg.Done()
println("Hello, World!")
}()
wg.Wait()
}
//輸出
$ go run main.go
0
1
2
3
Hello, World!
4
5
Channel
引用類型Channel是CSP模型的具體實現,用于多個goruntine之間進行通訊。其內部實現了同步,確保了并發安全。默認為同步模式,需要發送和接收配對。否則會被阻塞,直到另一方準備好后被喚醒。
func main() {
data := make(chan int) // 數據交換隊列
exit := make(chan bool) // 退出通知
go func() {
for d := range data {// 從隊列迭代接收數據,直到 close 。
fmt.Println(d)
}
fmt.Println("recv over.")
exit <- true// 發出退出通知。
}()
data <- 1// 發送數據。
data <- 2
data <- 3
close(data)// 關閉隊列。
fmt.Println("send over.")
<-exit// 等待退出通知。
}
//輸出:
1
2
3
send over.
recv over.
異步方式通過判斷緩沖區來決定是否阻塞。如果緩沖區已滿,發送被阻塞;緩沖區為空,接收被阻塞。通常情況下,異步channel可減少排隊阻塞,具備更高的效率。但應該考慮使用指針規避大對象拷貝,將多個元素打包,減少緩沖區大小等。
func main() {
data := make(chan int, 3)// 緩沖區可以存儲 3 個元素
exit := make(chan bool)
data <- 1// 在緩沖區未滿前,不會阻塞。
data <- 2
data <- 3
go func() {
for d := range data {// 在緩沖區未空前,不會阻塞。
fmt.Println(d)
}
exit <- true
}()
data <- 4// 如果緩沖區已滿,阻塞。
data <- 5
close(data)
<-exit
}
緩沖區是內部屬性,并非類型構成要素。
var a, b chan int = make(chan int), make(chan int, 3)
除用用 range 外,還可用 ok-idiom 模式判斷 channel 是否關閉。
for {
if d, ok := <-data; ok {
fmt.Println(d)
} else {
break
}
}
向 closed channel 發送數據引發 panic 錯誤,接收立即返回零值。而 nil channel,無論收發都會被阻塞。內置函數 len 返回未被讀取的緩沖元素數量,cap 返回緩沖區大小。
單向
可以將channel隱式轉換為單身隊列,只收或只發。
c := make(chan int, 3)
var send chan <- int = c // send only
var recv <- chan int = c // receiver only
選擇
如果需要同時處理多個channel,可以用select語句,它隨機選擇一個可用的channel做收發操作,或執行default case。
func main() {
a, b := make(chan int, 3), make(chan int)
go func() {
v, ok, s := 0, false, ""
for {
select {// 隨機選擇可用用 channel,接收數據。
case v, ok = <-a: s = "a"
case v, ok = <-b: s = "b"
}
if ok {
fmt.Println(s, v)
} else {
os.Exit(0)
}
}
}()
for i := 0; i < 5; i++ {
select {// 隨機選擇可用用 channel,發送數據。
case a <- i:
case b <- i:
}
}
close(a)
select {}// 沒有可用用 channel,阻塞 main goroutine。
}
//輸出:
b 3
a 0
a 1
a 2
b 4
模式
用簡單工廠模式打包并發任務和channel。
func NewTest() chan int {
c := make(chan int)
rand.Seed(time.Now().UnixNano())
go func() {
time.Sleep(time.Second)
c <- rant.Int()
}()
return c
}
func main() {
t := NewTest()
println(<-t) //等待gorountime結束返回。
}
用channel實現信號量(semaphore)。
func main() {
wg := sync.WaitGroup{}
wg.Add(3)
sem := make(chan int, 1)
for i := 0; i < 3; i++ {
go func(id int) {
defer wg.Done()
sem <- 1 // 向 sem 發送數據,阻塞或者成功。
for x := 0; x < 3; x++ {
fmt.Println(id, x)
}
<- sem // 接收數據,使得其他阻塞 goroutine 可以發送數據。
}(i)
}
wg.Wait()
}
用closed channel發出退出通知。
func main() {
var wg sync.WaitGroup
quit := make(chan bool)
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
task := func() {
println(id, time.Now().Nanosecond())
time.Sleep(time.Second)
}
for {
select {
case <- quit: // closed channel 不會阻塞,因此可用作退出通知。
return
default://執行正常任務
task()
}
}
}(i)
}
time.Sleep(time.Second * 5) // 讓測試 goroutine 運行一會。
close(quit) // 發出退出通知。
wg.Wait()
}
用select 實現超時 (timeout)。channel 是第一類對象,可傳參 (內部實現為指針) 或者作為結構成員。
type Request struct {
data []int
ret chan int
}
func NewRequest(data ...int) *Request {
return &Request{ data, make(chan int, 1) }
}
func Process(req *Request) {
x := 0
for _, i := range req.data {
x += i
}
req.ret <- x
}
func main() {
req := NewRequest(10, 20, 30)
Process(req)
fmt.Println(<-req.ret)
}