- 原文地址:Part 21: Goroutines
- 原文作者:Naveen R
- 譯者:咔嘰咔嘰 轉載請註明出處。
在前面的教程中,我們討論了併發以及它與並行的不同之處。在本教程中,我們將討論如何使用Goroutines
在 Go 中實現併發性。
什麼是Goroutines
Goroutines
是與其他函式或方法同時執行的函式或方法。 Goroutines
可以被認為是輕量級執行緒。與執行緒相比,建立Goroutine
的成本很小。因此,Go 應用程式通常可以輕鬆執行數千個Goroutines
。
Goroutines
相較執行緒的優勢
- 與執行緒相比,
Goroutines
非常輕量化。它們的堆疊大小隻有幾 kb,堆疊可以根據應用程式的需要而伸縮,而線上程的情況下,堆疊必須固定指定大小。 Goroutines
被複用到較少數量的系統執行緒。程式中可能一個執行緒有數千個Goroutines
。如果該執行緒中的任何Goroutine
阻塞,則建立另一個系統執行緒,並將剩餘的Goroutines
移動到新的執行緒。所有這些都由執行時處理,Go 從這些複雜的細節中抽象出來一個簡潔的 API 來原生支援併發。Goroutines
使用channel
進行通訊。Goroutines
使用channel
通訊可以避免因訪問共享記憶體而發生競態條件。channel
可以被認為是Goroutines
通訊的管道。我們將在下一個教程中詳細討論channel
。
如何啟動Goroutines
使用關鍵字go
對函式或方法呼叫進行字首修飾,就可以執行新的Goroutine
了。
來建立一個Goroutine
吧:)
package main
import (
"fmt"
)
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
fmt.Println("main function")
}
複製程式碼
在第 11 行,go hello()
啟動了一個新的Goroutine
。現在hello
函式將與main
函式一起併發執行。main
在其自己的Goroutine
中執行,並把main
函式執行的Goroutine
為main Goroutine
主協程。
執行這個程式,你會有一個驚喜!
該程式僅輸出了main
函式的文字。我們啟動的Goroutine
怎麼了?我們需要了解go
協程的兩個主要屬性,就知道為什麼會發生這種情況了。
- 當一個新的
Goroutine
啟動時,Goroutine
呼叫立即返回。與函式不同,控制器不會等待Goroutine
完成執行。在Goroutine
呼叫之後,控制器立即返回到下一行程式碼,並忽略了Goroutine
的任何返回值。 main Goroutine
控制該程式的任何其他Goroutines
執行。如果main Goroutine
終止,那麼程式將被終止,其他Goroutine
將可能得不到執行。
我猜你能夠理解為什麼我們的Goroutine
沒有被執行。在第 11 行呼叫go hello()
,控制器立即執行下一行程式碼並不等待hello goroutine
執行,然後列印了main function
之後main Goroutine
終止。沒有等待時間,因此你的Goroutine
沒有時間執行。
我們簡單解決一下這個問題。
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
協程休眠。在這種情況下,main Goroutine
進入休眠狀態 1 秒鐘。現在呼叫go hello()
有足夠的時間在main Goroutine
終止之前執行。該程式首先列印Hello world goroutine
,然後等待 1 秒然後列印main function
。
這種在mian Goroutine
中使用sleep
等待其他Goroutines
完成執行的方式只是我們用來理解Goroutines
如何工作的,正常情況下肯定不能這麼做。channels
可用於阻塞main Goroutine
,直到所有其他Goroutines
完成執行。我們將在下一個教程中討論channel
。
啟動多個Goroutines
再寫一個程式,啟動多個Goroutines
以更好地理解它。
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 行分別啟動了兩個Goroutines
,這兩個Goroutines
同時執行。numbers Goroutine
最初睡眠 250 毫秒然後列印 1,然後再次睡眠並列印 2,並且迴圈直到它列印 5。類似地,alphabets Goroutine
從 a 到 e 列印字母並且是 400 毫秒的睡眠時間。main Goroutine
在啟動alphabets
和numbers Goroutines
後睡眠 3000 毫秒,然後終止。
輸出,
1 a 2 3 b 4 c 5 d e main terminated
複製程式碼
下圖描繪了該程式的工作原理。請在新標籤頁中開啟圖片以獲得更好的可視性 :)
藍色影象的第一部分代表numbers Goroutine
,栗色的第二部分代表alphabets Goroutine
,綠色的第三部分代表main Goroutine
,黑色的合併了上述三個並向我們展示如何程式如何執行的。每個框頂部的 0 ms,250 ms 等字串表示以毫秒為單位的時間,輸出在每個框的底部,例如 1, 2, 3 等等。藍色框告訴我們在 250 毫秒時列印 1,在 500 毫秒時列印 2,依此類推。黑色框底部的值為 1 a 2 3 b 4 c 5 d e 然後main
終止,這也是程式的輸出。希望能通過這個圖理解該程式的工作原理。