這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
go 語言的一個很大的優勢就是可以方便地編寫並發程式。go 語言內建了 goroutine 機制。這是一種類似 coroutaine(協程) 的東西。但是又不完全相同。
比如這個例子:
package mainimport ("fmt""strconv")func main() {ch := make(chan int)task("A", ch)task("B", ch)fmt.Printf("begin\n")<-ch<-ch}func task(name string, ch chan int) {go func() {i := 1for {fmt.Printf("%s %d\n", name, i)i++}ch <- 1}()}
運行以後,發現會 A B 兩個 goroutine 會交替執行,並不像傳統的協程需要手動 schedule 。看起來很神奇。
稍稍改一下代碼,把
fmt.Printf("%s %d\n", name, i)
改成
print(name + " " + strconv.Itoa(i) + "\n")
再看看。神奇的效果消失了,只有 A 被運行。
那麼 fmt.Printf 和 print 有什麼差別,導致了這個結果呢?
大致翻了一下 go 的代碼,看出 go 語言在對 c lib 的封裝上用了個 cgo 的方式。而在通過 cgo 調用 c 庫的時候,會在調用時自動 schedule 切換走,在調用結束的時候再返回。這兩個結果的差異就在於,fmt.Printf 是通過 cgo 封裝的,而 print 則是原生實現的。所以在調用 fmt.Printf 的時候,就自動實現了調度。
傳統的 coroutaine 在訪問網路、資料庫等 io 操作時仍然會阻塞,失去並發能力,所以通常需要結合非同步 io 來實現。而現有的庫如果本身未提供非同步功能,就很難辦,往往需要重新實現。而且,即使有非同步 io 功能,也需要額外的開發,才能在表現上和以往順序程式相同的方式。
go 語言的這種實現方式很好的解決了這個問題,可以充分利用現有的大量 c 庫來封裝。
同時,也還是可以使用 runtime.Gosched() 來手動調度。在運算量大的情境下,也還是必要的。
在使用 print 的例子裡,如果使用 runtime.GOMAXPROCS(2),又可以重新並行起來。這時,兩個 goroutine 是在兩個獨立的線程中啟動並執行。這又是 goroutine 和協程的一個不同點。不過啟用多個線程並不見得能讓程式更快。因為跨線程的環境切換代價也是很大的。在上面那個簡單的例子裡,還會讓程式變得更慢。降低 環境切換開銷也是協程的一個優勢所在。