Go語言 協程

導言

  • 原文鏈接: Part 21: Goroutines
  • If translation is not allowed, please leave me in the comment area and I will delete it as soon as possible.

協程

在這一節中,我們來討論一下,Go語言 是怎麼使用協程實現併發的。

協程是什麼?

協程就是一個函數或方法 — 這個 函數或方法 與其他的 函數或方法 併發執行。協程可以看做輕量級線程。與線程相比,創建協程的耗費很少。因此,一個 Go 應用可以擁有成千上萬的協程。

與線程相比,協程具有的優勢

  1. 與線程相比,協程的耗費十分低廉。它們的堆棧只有 幾kb,而且它們的堆棧能根據應用需要,進行擴大或縮小。然而,線程的棧空間必須被指定,而且是固定的。
  2. 協程是系統線程的多路複用。一個具有成千上萬協程的程序,可能只有一個線程。假如運行在線程上的協程阻塞了 — 例如等待用戶輸入,此時將會有新的系統線程被創建,且餘下的協程會被移向這個新線程。這些複雜的事情交代給運行環境就可以了,作爲程序員,我們將擁有一個簡單的 API,這個 API 已經隱藏了複雜的細節,使我們可以方便地進行併發作業。
  3. 協程們使用通道進行通信。在多協程併發環境中,通道可以避免競態條件的發生,即防止協程對共享內存的 “同時” 訪問。通道可以認爲是一根管道,這根管道可供協程們進行數據傳輸。 (這比較抽象,之後再說管道是啥)

怎麼開啓一個協程?

只需在函數或方法前,添加一個關鍵字 go,你就能運行一個協程。

接下來,我們創建一個協程吧~

package main

import (  
    "fmt"
)

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

在第 11 行,go hello() 開啓了一個新協程。現在,hello函數 將與 main函數 一起併發運行。main函數 在其自己的協程中運行,這個協程叫做 main協程 — main Goroutine

運行這個程序,你將會大吃一驚!

程序只會輸出: main function,我們自己創建的協程呢?

在理解了協程的兩個主要特性後,我們才能知道發生了什麼。

  1. 當開啓新協程時,協程的調用將會立即返回。與函數不同,控件並不會關心協程是否完成工作。控件會馬上轉向協程調用的下一句代碼,並不會等待協程工作的完成,協程的返回值會被忽略。
  2. main協程 運行時,其他協程才能運行。如果 main協程 終止了,程序也將被終止,其它協程將無法運行。

我想,現在你應該可以理解,爲什麼我們創建的協程沒有運行了。在第 11 行,在 go hello() 後,控件立即轉向下一行,它並不會等待 hello協程 完成工作。在第 12 行打印了 main function 後,因爲已經沒有可運行的代碼了,於是 main協程 終止了。最終,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函數,它能讓正在運行的協程休眠一段時間。在這個例子中,main協程 被強制休眠 1 秒。此時,在 main協程終止前,hello協程 將擁有足夠的運行時間。這個程序將首先打印 Hello world goroutine,並在 1 秒後打印 main function

main協程 中使用 Sleep函數,這是一個奇技淫巧,它能讓我們理解協程們怎麼工作。當然,我們也可以使用通道去阻塞 main協程,這也可以保證其他協程能完成它們的工作。

啓動多個協程

爲了更好的理解協程,我們來寫一個程序,這個程序可以開啓多個協程。

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")
}

在上面程序的第 2122 行,我們開啓了 2 個協程。這 2 個協程將會併發地運行。numbers協程 會在休眠 250 毫秒後,打印 1,之後再次進入休眠狀態,休眠結束再打印 2,這個循環將會一直髮生,直到打印了 5。同樣地,alphabets協程 會打印 ae,它擁有 400 毫秒的休眠時間。main協程 會在初始化 numbers協程 和 alphabets協程 後,休眠 3000 毫秒,之後進入終止狀態。

運行這個程序,輸出如下:

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

下圖描繪了程序的運行細節,在新頁面查看這個圖片將會更清晰喔。
在這裏插入圖片描述
在圖片中,藍色框表示 numbers協程,栗色框表示 alphabets協程,綠色框表示 main協程。而最底的黑色框,統一了上述的 3 個協程,它展示了這 3 個協程的執行細節。黑色框頂部的字符串,如 0 ms250 ms,表示的是時間點,而框底部的字符串表示的是輸出。藍框給予了我們如下信息:在 250 ms後,1 會被打印,在 500 ms 後,2 會被打印,以此類推。

黑色框底部的 1 a 2 3 b 4 c 5 d e main terminated,就是程序的輸出。

圖片不言自明,通過這個圖片,你將能理解這個程序做了什麼。

這就是協程,祝你愉快~

原作者留言

優質內容來之不易,您可以通過該 鏈接 爲我捐贈。

最後

感謝原作者的優質內容。

這是我的第四次翻譯,歡迎指出文中的任何錯誤。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章