1. 程式人生 > >21.go協程(Goroutine)

21.go協程(Goroutine)

歡迎來到Golang 系列教程的第 21 章。

在前面的教程裡,我們探討了併發,以及併發與並行的區別。本教程則會介紹在 Go 語言裡,如何使用 Go 協程(Goroutine)來實現併發。

Go 協程是什麼?

Go 協程是與其他函式或方法一起併發執行的函式或方法。Go 協程可以看作是輕量級執行緒。與執行緒相比,建立一個 Go 協程的成本很小。因此在 Go 應用中,常常會看到有數以千計的 Go 協程併發地執行。

Go 協程相比於執行緒的優勢

  • 相比執行緒而言,Go 協程的成本極低。堆疊大小隻有若干 kb,並且可以根據應用的需求進行增減。而執行緒必須指定堆疊的大小,其堆疊是固定不變的。
  • Go 協程會複用(Multiplex)數量更少的 OS 執行緒。即使程式有數以千計的 Go 協程,也可能只有一個執行緒。如果該執行緒中的某一 Go 協程發生了阻塞(比如說等待使用者輸入),那麼系統會再建立一個 OS 執行緒,並把其餘 Go 協程都移動到這個新的 OS 執行緒。所有這一切都在執行時進行,作為程式設計師,我們沒有直接面臨這些複雜的細節,而是有一個簡潔的 API 來處理併發。
  • Go 協程使用通道(Channel)來進行通訊。通道用於防止多個協程訪問共享記憶體時發生競態條件(Race Condition)。通道可以看作是 Go 協程之間通訊的管道。我們會在下一教程詳細討論通道。

如何啟動一個 Go 協程?

呼叫函式或者方法時,在前面加上關鍵字 go,可以讓一個新的 Go 協程併發地執行。

讓我們建立一個 Go 協程吧。

package main

import (
    "fmt"
)

func hello() {
    fmt.Println("Hello world goroutine")
}
func main() {
    go hello()
    fmt.Println("main function")
}

在第 11 行,go hello() 啟動了一個新的 Go 協程。現在 hello() 函式與 main() 函式會併發地執行。主函式會執行在一個特有的 Go 協程上,它稱為 Go 主協程(Main Goroutine)。

執行一下程式,你會很驚訝!

該程式只會輸出文字 main function。我們啟動的 Go 協程究竟出現了什麼問題?要理解這一切,我們需要理解兩個 Go 協程的主要性質。

  • 啟動一個新的協程時,協程的呼叫會立即返回。與函式不同,程式控制不會去等待 Go 協程執行完畢。在呼叫 Go 協程之後,程式控制會立即返回到程式碼的下一行,忽略該協程的任何返回值。
  • 如果希望執行其他 Go 協程,Go 主協程必須繼續執行著。如果 Go 主協程終止,則程式終止,於是其他 Go 協程也不會繼續執行。

現在你應該能夠理解,為何我們的 Go 協程沒有運行了吧。在第 11 行呼叫了 go hello() 之後,程式控制沒有等待 hello 協程結束,立即返回到了程式碼下一行,列印 main function。接著由於沒有其他可執行的程式碼,Go 主協程終止,於是 hello 協程就沒有機會運行了。

我們現在修復這個問題。

package main

import (  
    "fmt"
    "time"
)

func hello() {  
    fmt.Println("Hello world goroutine")
}
func main() {  
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

在上面程式的第 13 行,我們呼叫了 time 包裡的函式 Sleep,該函式會休眠執行它的 Go 協程。在這裡,我們使 Go 主協程休眠了 1 秒。因此在主協程終止之前,呼叫 go hello() 就有足夠的時間來執行了。該程式首先列印 Hello world goroutine,等待 1 秒鐘之後,接著列印 main function

在 Go 主協程中使用休眠,以便等待其他協程執行完畢,這種方法只是用於理解 Go 協程如何工作的技巧。通道可用於在其他協程結束執行之前,阻塞 Go 主協程。我們會在下一教程中討論通道。

啟動多個 Go 協程

為了更好地理解 Go 協程,我們再編寫一個程式,啟動多個 Go 協程。

package main

import (  
    "fmt"
    "time"
)

func numbers() {  
    for i := 1; i <= 5; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Printf("%d ", i)
    }
}
func alphabets() {  
    for i := 'a'; i <= 'e'; i++ {
        time.Sleep(400 * time.Millisecond)
        fmt.Printf("%c ", i)
    }
}
func main() {  
    go numbers()
    go alphabets()
    time.Sleep(3000 * time.Millisecond)
    fmt.Println("main terminated")
}

在上面程式中的第 21 行和第 22 行,啟動了兩個 Go 協程。現在,這兩個協程併發地執行。numbers 協程首先休眠 250 微秒,接著列印 1,然後再次休眠,列印 2,依此類推,一直到列印 5 結束。alphabete 協程同樣列印從 ae 的字母,並且每次有 400 微秒的休眠時間。 Go 主協程啟動了 numbersalphabete 兩個 Go 協程,休眠了 3000 微秒後終止程式。

該程式會輸出:

1 a 2 3 b 4 c 5 d e main terminated  

程式的運作如下圖所示。為了更好地觀看圖片,請在新標籤頁中開啟。

image

第一張藍色的圖表示 numbers 協程,第二張褐紅色的圖表示 alphabets 協程,第三張綠色的圖表示 Go 主協程,而最後一張黑色的圖把以上三種協程合併了,表明程式是如何執行的。在每個方框頂部,諸如 0 ms250 ms 這樣的字串表示時間(以微秒為單位)。在每個方框的底部,123 等表示輸出。藍色方框表示:250 ms 打印出 1500 ms 打印出 2,依此類推。最後黑色方框的底部的值會是 1 a 2 3 b 4 c 5 d e main terminated,這同樣也是整個程式的輸出。以上圖片非常直觀,你可以用它來理解程式是如何運作的。

Go 協程的介紹到此結束。祝你愉快。