1. 程式人生 > >Golang 之 協程 goroutine(二)

Golang 之 協程 goroutine(二)

“子程式就是協程的一種特例。” - - Donald Knuth

這裡寫圖片描述

  • 普通函式,一個執行緒內有個main函式調一個叫doSomeWork的函式,等doSomeWork做完以後才會將控制權交還給main函式,然後main函式執行下一個語句

  • 協程,main和doSomeWork之間有個雙向通道,資料與控制權可以雙向流通,可能放在一個或者多個執行緒裡

這裡寫圖片描述

搶佔式多工處理

  • 計算機只有一個處理器,巨集觀上,我們卻可以看到數以百計的執行緒正同時執行在機器上,這就是搶佔式多工處理的效果,通過作業系統核心通過對執行緒的排程(排程器作為核心的一部分),將時間切片,分成一段段的時間片。這些時間間隔以毫秒為精度且長度並不固定。針對每個處理器,每個時間片僅服務於單獨一個執行緒。執行緒的迅速執行給我們造成了它們在同時執行的假象。我們在兩個時間片的間隔中進行上下文切換。該方法的優點在於,那些正在等待某些作業系統資源的執行緒將不會浪費時間片,直到資源有效為止。
  • 在此種方式下執行緒將被系統強制性中斷。在上下文切換的過程中,作業系統會在下一個執行緒將要執行的程式碼中插入一條跳轉到下一個上下文切換的指令。該指令是一個軟中斷,如果執行緒在遇到這條指令前就終止了(例如,它正在等待某個資源),那麼該指定將被刪除而上下文切換也將提前發生。

  • 搶佔式多工處理的主要缺點在於,必須使用一種同步機制來保護資源以避免它們被無序訪問。除此之外,還有另一種多工管理模型,被稱為協調式多工管理,其中執行緒間的切換將由執行緒自己負責完成。該模型普遍認為太過危險,原因在於執行緒間的切換不發生的風險太大。但Windows作業系統僅僅實現了搶佔式多工處理

協程:輕量級執行緒

  • 非搶佔式多工處理,由協程主動交出控制權,因此才能做到輕量級(沒有過多的上下文狀態的儲存)

  • 在golang中,協程只是編譯器,直譯器層面的多工(排程器排程協程)

  • 多個協程可能在一個或多個執行緒上執行

10000個協程併發輸出,printf有IO操作,IO操作時協程會釋放控制權,因此可以看到10000併發輸出,開的執行緒數不會超過核心數。

package main

import (
    "fmt"
    "time"
)

func main()  {
    for i:= 0; i<10000; i++ {
        go func(j int
) { for { fmt.Printf("goroutine from %d\n", j) } }(i) } // main 與 go 後面的立即執行函式是併發執行,此處讓main函式等待10分鐘 ,防止main先執行完退出 // 從而殺掉 go 後面的立即函式 time.Sleep(time.Minute*10) }

無IO併發,無控制權釋放

package main

import (
    "fmt"
    "time"
)

func main()  {

    var a [10]int
    fmt.Println(a)
    for i:= 0; i<10; i++ {
        go func(j int) {
            for {
                a[j]++
            }
        }(i)
    }

    time.Sleep(time.Minute*10)
    fmt.Println(a)
}
//結果:
//[0 0 0 0 0 0 0 0 0 0]

這裡寫圖片描述

手動交出控制權。10個協程都能獲得控制權,做自增運算

"""  goroutine.go """
package main

import (
    "time"
    "runtime"
    "fmt"
)

func main()  {


    var a [10]int
    fmt.Println(a)
    for i:= 0; i<10; i++ {
        // 傳值,防止閉包,導致index out of range
        go func(j int) {
            for {
                a[j]++
                // 手動交出控制權
                runtime.Gosched()
            }
        }(i)
    }

    time.Sleep(time.Second * 3)
    fmt.Println(a)
}

//結果:
//[0 0 0 0 0 0 0 0 0 0]
//[729611 707997 728874 704561 709436 705376 708068 703098 710653 711052]
  • go run -race goroutine.go,此處26行與18行有讀寫會有異常,但不會報錯,當在fmt.Println(a),讀的時候,a[j]++可能正在寫,這就需要channel來解決問題
==================
WARNING: DATA RACE
Read at 0x00c042092000 by main goroutine:
  main.main()
      E:/GOProject/src/10_騫惰/goroutine.go:26 +0x1ec

Previous write at 0x00c042092000 by goroutine 6:
  main.main.func1()
      E:/GOProject/src/10_騫惰/goroutine.go:18 +0x72

Goroutine 6 (running) created at:
  main.main()
      E:/GOProject/src/10_騫惰/goroutine.go:16 +0x1b3
==================
[729591 717311 717799 722817 721530 722732 721418 722303 717897 713184]
Found 1 data race(s)
exit status 66

此處注意因閉包而產生的報錯問題

package main

import (
    "time"
    "fmt"
)

func main()  {

    var a [10]int
    fmt.Println(a)
    for i:= 0; i<10; i++ {

        go func() {
            a[i]++
        }()
    }

    time.Sleep(time.Second * 3)
    fmt.Println(a)
}
// panic: 
// runtime error: index out of range

其他語言

  • C++:Boost.Coroutine
  • Java:原生不支援,第三JVM有解決方案
  • Python:yield關鍵字模擬協程, Python 3.5:async def 對協程原生支援

定義goroutine,任何函式只需加上go就能送給排程器執行,不需要再定義時區分是非同步函式(不同於Python 3.5:async),排程器在合適的點切換

這裡寫圖片描述

合適的點切換(僅供參考)

  • IO,select
  • channel
  • 等待鎖
  • 函式呼叫(有時),有排程器決定
  • runtime.Gosched