人是一種高併發的物種,細品。
初識
對 Go 語言的第一印象就是其原生地支援併發程式設計,而且使用的是協程,比執行緒更加輕量。
關於程式、執行緒和協程的區別
- 程式是“程式執行的一個例項” ,擔當分配系統資源的實體。程式建立必須分配一個完整的獨立地址空間。程式切換隻發生在核心態。
- 執行緒:執行緒是程式的一個執行流,獨立執行它自己的程式程式碼,是程式執行流的最小單元,是處理器排程和分派的基本單位。一個程式可以有一個或多個執行緒。
- 協程:協程不是程式或執行緒,其執行過程更類似於子例程,或者說不帶返回值的函式呼叫。在語言級別可以建立併發協程,然後編寫程式碼去進行管理。Go 將這一步承包下來,使協程併發執行成本更低。
Go 實現最簡單的併發
for i := 0; i < 10; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
專案實踐
最近有個專案需要同時呼叫多個 job,並要等待這些 job 完成之後才能往下執行。
序列執行 job
最開始,我們擁有一個執行 job 的方法,並且序列執行所有的 job:
func buildJob(name string) {
...
}
buildJob("A")
buildJob("B")
buildJob("C")
並行執行 job
因為所有 job 是可以併發執行的,這樣就不用必須等待上一個 job 執行完成後,才能繼續執行其他 job。我們可以使用 Go 語言的關鍵字 go
來快速啟用一個 goroutine
,下面我們將併發地執行三個 job:
go buildJob("A")
go buildJob("B")
go buildJob("C")
等待所有 job 完成
怎樣才能知道每個 job 是否已經完成,這裡可以使用 channel
進行通訊,並使用 select
檢查執行結果:
func buildJob(ch chan error, name string) {
var err error
... // build job
ch <- err // finnaly, send the result into channel
}
func build() error {
jobCount := 3
errCh := make(err chan error, jobCount)
defer close(errCh) // 關閉 channel
go buildJob(errCh, "A")
go buildJob(errCh, "B")
go buildJob(errCh, "C")
for {
select {
case err := <-ch:
if err != nil {
return err
}
}
jobCount--
if jobCount <= 0 {
break
}
}
return nil
}
發現問題
當 job A 執行失敗時,build
方法會 return err
退出,並執行 close(errCh)
。可是此時另外兩個 job B 和 C 可能還沒執行完成,同時也會把結果發給 errCh
,但由於這個時候 errCh
已經被關閉了,會導致程式退出 panic: send on closed channel
。
優化程式碼
在給 channel 傳送資料的時候,可以使用接收資料的第二個值判斷 channel 是否關閉:
func buildJob(ch chan error, name string) {
var err error
... // build job
if _, ok := <-ch; !ok {
return
}
ch <- err // finnaly, send the result into channel
}
func build() error {
jobCount := 3
errCh := make(err chan error, jobCount)
defer close(errCh) // 關閉 channel
go buildJob(errCh, "A")
go buildJob(errCh, "B")
go buildJob(errCh, "C")
for {
select {
case err := <-ch:
if err != nil {
return err
}
}
jobCount--
if jobCount <= 0 {
break
}
}
return nil
}
總結
Go 併發程式設計看似只需要一個關鍵字 go
就可以跑起來一個 goroutine
,但真正實踐中,還是有需要問題需要去處理的。
原文連結:k8scat.com/posts/code-with-golang-...
本作品採用《CC 協議》,轉載必須註明作者和本文連結