Golang 入門 : goroutine(協程)

在操做系統中,執行體是個抽象的概念。與之對應的實體有進程、線程以及協程(coroutine)。協程也叫輕量級的線程,與傳統的進程和線程相比,協程的最大特色是 "輕"!能夠輕鬆建立上百萬個協程而不會致使系統資源衰竭。
多數編程語言在語法層面並不直接支持協程,而是經過庫的方式支持。可是用庫的方式支持的功能每每不是很完整,好比僅僅提供輕量級線程的建立、銷燬和切換等能力。若是在這樣的協程中調用一個同步 IO 操做,好比網絡通訊、本地文件讀寫,都會阻塞其餘的併發執行的協程,從而沒法達到輕量級線程自己指望達到的目標。html

goroutine

Golang 在語言級別支持協程,稱之爲 goroutine。Golang 標準庫提供的全部系統調用操做(包括全部的同步 IO 操做),都會出讓 CPU 給其餘 goroutine。這讓 goroutine 的切換管理不依賴於系統的線程和進程,也不依賴於 CPU 的核心數量,而是交給 Golang 的運行時統一調度。算法

goroutine 是 Golang 中併發設計的核心,更多關於併發的概念,請參考《Golang 入門 : 理解併發與並行》。 本文接下來的部分着重經過 demo 介紹 goroutine 的用法。編程

入門 demo

要在一個協程中運行函數,直接在調用函數時添加關鍵字 go 就能夠了:網絡

package main

import (
    "time"
    "fmt"
)

func say(s string) {
    for i := 0; i < 3; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("hello world")
    time.Sleep(1000 * time.Millisecond)
    fmt.Println("over!")
}

執行上面的代碼,輸出結果爲:併發

hello world
hello world
hello world
over!

至於爲何要在 main 函數中調用 Sleep,如何用優雅的方式代替 Sleep,請參考《Golang 入門 : 等待 goroutine 完成任務》一文。編程語言

goroutine 的生命週期

讓咱們經過下面的 demo 來理解 goroutine 的生命週期:函數

package main

import (
    "runtime"
    "sync"
    "fmt"
)

func main() {
    // 分配一個邏輯處理器給調度器使用
    runtime.GOMAXPROCS(1)

    // wg用來等待程序完成
    // 計數加2,表示要等待兩個goroutine
    var wg sync.WaitGroup
    wg.Add(2)

    fmt.Println("Start Goroutines")

    // 聲明一個匿名函數,並建立一個goroutine
    go func(){
        // 在函數退出時調用Done來通知main函數工做已經完成
        defer wg.Done()

        // 顯示字母表3次
        for count := 0; count< 3; count++{
            for char := 'a'; char< 'a'+26; char++{
                fmt.Printf("%c ", char)
            }
            fmt.Println()
        }
    }()
    // 聲明一個匿名函數,並建立一個goroutine
    go func(){
        // 在函數退出時調用Done來通知main函數工做已經完成
        defer wg.Done()

        // 顯示字母表3次
        for count := 0; count< 3; count++{
            for char := 'A'; char< 'A'+26; char++{
                fmt.Printf("%c ", char)
            }
            fmt.Println()
        }
    }()

    // 等待goroutine結束
    fmt.Println("Waiting To Finish")
    wg.Wait()

    fmt.Println("Terminating Program")
}

在 demo 的起始部分,經過調用 runtime 包中的 GOMAXPROCS 函數,把可使用的邏輯處理器的數量設置爲 1。
接下來經過 goroutine 執行的兩個匿名函數分別輸出三遍小寫字母和三遍大寫字母。運行上面代碼,輸出的結果以下:spa

Start Goroutines
Waiting To Finish
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
Terminating Program

第一個 goroutine 完成全部任務的時間過短了,以致於在調度器切換到第二個 goroutine 以前,就完成了全部任務。這也是爲何會看到先輸出了全部的大寫字母,以後才輸出小寫字母。咱們建立的兩個 goroutine 一個接一個地併發運行,獨立完成顯示字母表的任務。
由於 goroutine 以非阻塞的方式執行,它們會隨着程序(主線程)的結束而消亡,因此咱們在 main 函數中使用 WaitGroup 來等待兩個 goroutine 完成他們的工做,更多 WaitGroup 相關的信息,請參考《Golang 入門 : 等待 goroutine 完成任務》一文。操作系統

基於調度器的內部算法,一個正運行的 goroutine 在工做結束前,能夠被中止並從新調度。調度器這樣作的目的是防止某個 goroutine 長時間佔用邏輯處理器。當 goroutine 佔用時間過長時,調度器會中止當前正運行的 goroutine,並給其餘可運行的 goroutine 運行的機會。
讓咱們經過下圖來理解這一場景(下圖來自互聯網):線程

  • 在第 1 步,調度器開始運行 goroutine A,而 goroutine B 在運行隊列裏等待調度。
  • 在第 2 步,調度器交換了 goroutine A 和 goroutine B。因爲 goroutine A 並無完成工做,所以被放回到運行隊列。
  • 在第 3 步,goroutine B 完成了它的工做並被系統銷燬。這也讓 goroutine A 繼續以前的工做。

讓咱們經過一個運行時間長些的任務來觀察該行爲,運行下面的 代碼:

package main

import (
    "runtime"
    "sync"
    "fmt"
)

func main() {
    // wg用來等待程序完成
    var wg sync.WaitGroup

    // 分配一個邏輯處理器給調度器使用
    runtime.GOMAXPROCS(1)

    // 計數加2,表示要等待兩個goroutine
    wg.Add(2)

    // 建立兩個goroutine
    fmt.Println("Create Goroutines")
    go printPrime("A", &wg)
    go printPrime("B", &wg)

    // 等待goroutine結束
    fmt.Println("Waiting To Finish")
    wg.Wait()

    fmt.Println("Terminating Program")
}

// printPrime 顯示5000之內的素數值
func printPrime(prefix string, wg *sync.WaitGroup){
    // 在函數退出時調用Done來通知main函數工做已經完成
    defer wg.Done()

next:
    for outer := 2; outer < 5000; outer++ {
        for inner := 2; inner < outer; inner++ {
            if outer % inner == 0 {
                continue next
            }
        }
        fmt.Printf("%s:%d\n", prefix, outer)
    }
    fmt.Println("Completed", prefix)
}

代碼中運行了兩個 goroutine,分別打印 1-5000 內的素數,輸出的結果比較長,精簡以下:

Create Goroutines
Waiting To Finish
B:2
B:3
...
B:3851          
A:2             ** 切換 goroutine
A:3
...
A:4297          
B:3853          ** 切換 goroutine
...
B:4999
Completed B
A:4327          ** 切換 goroutine
...
A:4999
Completed A
Terminating Program

上面的輸出說明:goroutine B 先執行,而後切換到 goroutine A,再切換到 goroutine B 運行至任務結束,最後又切換到 goroutine A,運行至任務結束。注意,每次運行這個程序,調度器切換的時間點都會稍有不一樣。

讓 goroutine 並行執行

前面的兩個示例,經過設置 runtime.GOMAXPROCS(1),強制讓 goroutine 在一個邏輯處理器上併發執行。用一樣的方式,咱們能夠設置邏輯處理器的個數等於物理處理器的個數,從而讓 goroutine 並行執行(物理處理器的個數得大於 1)。
下面的代碼可讓邏輯處理器的個數等於物理處理器的個數:

runtime.GOMAXPROCS(runtime.NumCPU())

其中的函數 NumCPU 返回可使用的物理處理器的數量。所以,調用 GOMAXPROCS 函數就爲每一個可用的物理處理器建立一個邏輯處理器。注意,從 Golang 1.5 開始,GOMAXPROCS 的默認值已經等於可使用的物理處理器的數量了。
修改上面輸出素數的程序:

runtime.GOMAXPROCS(2)

由於咱們只建立了兩個 goroutine,因此邏輯處理器的數量設置爲 2 就能夠了,從新運行該程序,看看是否是 A 和 B 的輸出混合在一塊兒了:

...
B:1741
B:1747
A:241
A:251
B:1753
A:257
A:263
A:269
A:271
A:277
B:1759
A:281
...

除了這個 demo 程序,在真實場景中這種並行的方式會帶來不少數據同步的問題。接下來咱們將介紹如何來解決數據的同步問題。

參考:
《Go語言實戰》

相關文章
相關標籤/搜索