示例剖析golang中的CSP並發模型

1. 相關概念: 

用戶態:當一個進程在執行用戶自己的代碼時處於用戶運行態(用戶態)

內核態:當一個進程因為系統調用陷入內核代碼中執行時處於內核運行態(內核態),引入內核態防止用戶態的程序隨意的操作內核地址空間,具有一定的安全保護作用。這種保護模式是通過內存頁表操作等機制,保證進程間的地址空間不會相互沖突,一個進程的操作不會修改另一個進程地址空間中的數據。

用戶態與內核態之間的切換:當在系統中執行一個程序時,大部分時間都是運行在用戶態下的,在其需要操作系統幫助完成一些用戶態自己沒有特權和能力完成的操作時就會切換到內核態。有以下三種方式:

(1)系統調用(中斷)用戶態進程主動要求切換到內核態的一種方式。

(2)異常cpu運行時如果發生一些沒有預知的異常,會觸發當前進程切換到處理此異常的內核相關進程中。 

(3)外圍設備的中斷用戶態進程主動要求切換到內核態的一種方式。

協程:又稱微線程,纖程。英文名Coroutine。Coroutine是一種運行在用戶態的用戶線程,類似於 greenthread。協程與線程都相互獨立,且有自己的上下文,不同之處在於,協程的切換由其自身控制,而線程的切換收到系統調度。

2. CSP (通信順序進程) 

CSP模型用來描述兩個獨立的並發實體通過共享的通訊channel管道進行通信的並發模型。

golang借用瞭CSP模型的一些概念如:實體 process,通道 channel,為之實現並發進行瞭理論支持,實際上並沒有完全實現CSP模型的所有理論。process是在go語言上的表現就是goroutine,是實際上並發執行的實體,每個實體之間是通過channel通訊來實現數據共享。

3. channel:同步&傳遞消息

channel是被單獨創建並且可以在進程之間傳遞,它的通信模式類似於boss-worker模式,一個實體通過將消息發送到channel中,然後又監聽這個channel的實體處理,兩個實體之間是匿名的,實現原理上其實是一個阻塞的消息隊列。

具體可以分為:有/無緩存channel,隻讀channel,隻寫channel,雙向channel

寫操作:chan <- value

讀操作:<- chan

// Create channel
// Unbuffered channel
umbuffer_chan := make(chan int)
// Buffered channel
// Buffer Size = 3
buffer_chan := make(chan int,3)
// Read-Only channel
read_channel := make(&lt;-chan int)
// Receive-Only channel
receive_channel := make(chan&lt;- int)

生產者-消費者Sample:

package main
import (
   "fmt" 
   "time"
)
// 生產者
func Producer (queue chan&lt;- int){
        for i:= 0; i &lt; 10; i++ {
                queue &lt;- i
        }
}
// 消費者
func Consumer( queue &lt;-chan int){
        for i :=0; i &lt; 10; i++{
                v := &lt;- queue
                fmt.Println("receive:", v)
        }
}
func main(){
        queue := make(chan int, 1)
        go Producer(queue)
        go Consumer(queue)
        time.Sleep(1e9) //讓Producer與Consumer完成
}

4. goroutine:實際並發執行的實體

在函數或者方法前面加上關鍵字go,就創建瞭並發運行的goroutine,eg:

go func (){
}
func Test(){
}
// ...
go Test()

實例代碼:

package main  // 代碼包聲明語句。
import (
   "fmt" //系統包用來輸出的
   "math/rand"
   "runtime"
   "sync"
   "time"
)
func main() {
   // 分配一個邏輯處理器給調度器使用
   runtime.GOMAXPROCS(1)
   // WaitGroup是一個計數信號量,用來記錄和維護運行的goroutine,如果當前的wg&gt;0,對應的exit方法就會阻塞
   var wg sync.WaitGroup
   // 計數加2表示要等待兩個goroutine
   wg.Add(2)
   fmt.Printf("Start Goroutines \n", )
   // 聲明匿名函數,創建goroutine
   go func(){
      // 關鍵字defer會修改函數調用時機,在函數退出時調用Done來通知main函數工作已經完成
      defer wg.Done()
      for count:=0; count&lt;3; count++ {
         for char :='a'; char&lt;'a'+26 ; char++ {
            fmt.Printf("%c ", char)
         }
      }
   }()
   // 聲明匿名函數,創建goroutine
   go func() {
      // 函數退出時調用Done來通知main函數工作已經完成
      defer wg.Done()
      for count:=0; count&lt;3; count++ {
         for char :='A'; char&lt;'A'+26 ; char++ {
            fmt.Printf("%c ", char)
         }
      }
   }()
   fmt.Println("Waiting to finish!\n", )
   // 等待結束
   wg.Wait()
   fmt.Println("\nTerminate program! \n", )
}

5. golang調度器

OS在物理處理器上調度線程來運行,而golang在邏輯處理器上調度goroutine來運行。每個邏輯處理器都分別綁定到單個操作系統線程。

如果創建一個goroutine並準備運行,這個goroutine就會被放到調度器的全局運行隊列中。之後,調度器就會將隊列中的goroutine分配給一個邏輯處理器,並放到這個邏輯處理器對應的本地運行隊列中。本地運行隊列中的goroutine會一直等待,知道自己被分配到相應的邏輯處理器上運行。

eg:

其中:

M:Machine,一個M直接關聯瞭一個內核線程。

P:Processor,代表瞭M所需要的上下文環境,也就是處理用戶級代碼邏輯的處理器。

G:Goroutine,本質上是一種輕量級的線程–協程。

MPG模型,三者關系的宏觀圖為:

Processor的作用:

當內核線程阻塞的時候,由於上下文的存在,我們能夠直接放開其他線程,繼續去執行未阻塞的線程,例子如下:

如果當前,G0由於I/O,系統調用進行瞭阻塞,這個時候M0就可以放開其他的線程:

M0和G0進行系統調用,等待返回值,上下文P以及routine隊列交由M1進行執行。當M0執行系統調用結束後,M0會嘗試去steal("偷")一個上下文,如果不成功,M0就把它的G0放到一個全局的運行隊列中,然後將自己放到線程池或者轉入休眠狀態。

Global runqueue是各個上下文P在運行完自己的本地的goroutine runqueue後用來拉取新的goroutine的地方(steal working算法)。此外,P也會周期性的檢查Global runqueue上的goroutine,來防止全局上的goroutine因為得不到執行而餓死。

以上就是示例剖析golang中的CSP並發模型的詳細內容,更多關於golang CSP並發模型的資料請關註WalkonNet其它相關文章!

推薦閱讀: