1. 程式人生 > >Golang教程:(二十一)協程

Golang教程:(二十一)協程

上一篇教程中,我們討論了併發,以及併發和並行的區別。在這篇教程中我們將討論在Go中如何通過Go協程實現併發。

什麼是協程

Go協程(Goroutine)是與其他函式方法同時執行的函式或方法。可以認為Go協程是輕量級的執行緒。與建立執行緒相比,建立Go協程的成本很小。因此在Go中同時執行上千個協程是很常見的。

Go協程對比執行緒的優點

  • 與執行緒相比,Go協程的開銷非常小。Go協程的堆疊大小隻有幾kb,它可以根據應用程式的需要而增長和縮小,而執行緒必須指定堆疊的大小,並且堆疊的大小是固定的。
  • Go協程被多路複用到較少的OS執行緒。在一個程式中數千個Go協程可能只執行在一個執行緒中。如果該執行緒中的任何一個Go協程阻塞(比如等待使用者輸入),那麼Go會建立一個新的OS執行緒並將其餘的Go協程移動到這個新的OS執行緒。所有這些操作都是 runtime 來完成的,而我們程式設計師不必關心這些複雜的細節,只需要利用 Go 提供的簡潔的 API 來處理併發就可以了。
  • Go 協程之間通過通道(channel)進行通訊。通道可以防止多個協程訪問共享記憶體時發生竟險(race condition)。通道可以想象成多個協程之間通訊的管道。我們將在下一篇教程中介紹通道。

如何建立一個協程?

在函式或方法呼叫之前加上關鍵字 go,這樣便開啟了一個併發的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 function。我們建立的協程發生了什麼?我們需要了解Go協程的兩個屬性,以瞭解為什麼發生這種情況。

  • 當建立一個Go協程時,建立這個Go協程的語句立即返回。與函式不同,程式流程不會等待Go協程結束再繼續執行。程式流程在開啟Go協程後立即返回並開始執行下一行程式碼,忽略Go協程的任何返回值。
  • 在主協程存在時才能執行其他協程,主協程終止則程式終止,其他協程也將終止。

我想你已經知道了為什麼我們的協程為什麼沒有執行。在11行呼叫 go hello()後,程式的流程直接調轉到下一條語句執行,並沒有等待 hello 協程退出,然後列印 main function。接著主協程結束執行,不會再執行任何程式碼,因此 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 函式來使呼叫該函式所在的協程休眠。在這裡是讓主協程休眠1秒鐘。現在呼叫 go hello() 有了足夠的時間得以在主協程退出之前執行。該程式首先列印 Hello world goroutine,等待1秒鐘之後列印 main function

在主協程中使用 Sleep 函式等待其他協程結束的方法是不正規的,我們用在這裡只是為了說明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行開啟了兩個協程。現在這兩個協程同時執行。numbers 協程最初睡眠 250 毫秒,然後列印 1,接著再次睡眠然後列印2,以此類推,直到列印到 5。類似地,alphabets 協程列印從 a 到 e 的字母,每個字母之間相隔 400 毫秒。主協程開啟 numbersalphabets 協程,等待 3000 毫秒,最後終止。

程式的輸出為:

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

下面的圖片描述了這個程式是如何工作的,請在新的標籤中開啟影象以獲得更好的效果:)

這裡寫圖片描述

上圖中,藍色的線框表示 numbers 協程,栗色的線框表示 alphabets 協程。綠色的線框表示主協程。黑色的線框合併了上述三個協程,向我們展示了該程式的工作原理。每個框頂部的 0ms,250 ms 的字串表示以毫秒為單位的時間,在每個框底部的 1,2,3 表示輸出。
藍色的線框告訴我們在 250ms 的時候列印了1,在 500ms 的時候列印了2,以此類推。因此最後一個線框底部的輸出:1 a 2 3 b 4 c 5 d e main terminated 也是整個程式的輸出。上面的影象是很好理解的,您將能夠了解該程式的工作原理。

Go協程的介紹就到這裡。祝你有美好的一天!