文章來自 – 微信公眾號:Go 語言圈
一、生產者消費者模型
生產者消費者模型:某個模組(函式等〉負責產生資料,這些資料由另一個模組來負責處理(此處的模組是廣義的,可以是類、函式、協程、執行緒、程式等)。產生資料的模組,就形象地稱為生產者;而處理資料的模組,就稱為消費者。
單單抽象出生產者和消費者,還夠不上是生產者消費者模型。該模式還需要有一個緩衝區處於生產者和消費者之間,作為一箇中介。生產者把資料放入緩衝區,而消費者從緩衝區取出資料。
大概的結構如下圖。
假設你要寄一件快遞,大致過程如下。.
1.把快遞封好——相當於生產者製造資料。
2.把快遞交給快遞中心——相當於生產者把資料放入緩衝區。
3.郵遞員把快遞從快遞中心取出——相當於消費者把資料取出緩衝區。
這麼看,有了緩衝區就有了以下好處:
解耦:降低消費者和生產者之間的耦合度。有了快遞中心,就不必直接把快遞交給郵寄員,郵寄快遞的人不對郵寄員產生任何依賴,如果某一個天郵寄員換人了,對於郵寄快遞的人也沒有影響。
假設生產者和消費者分別是兩個類。如果讓生產者直接呼叫消費者的某個方法,那麼生產者對於消費者就會產生依賴(也就是耦合)。將來如果消費者的程式碼發生變化,可能會真接影響到生產者。而如果兩者都依賴於某個緩衝區,兩者之間不直接依賴,耦合度也就相應降低了。
併發:生產者消費者數量不對等,依然能夠保持正常通訊。由於函式呼叫是同步的(或者叫阻塞的),在消費者的方法沒有返回之前,生產者只好一直等在那邊。萬一消費者處理資料很慢,生產者只能等著浪費時間。使用了生產者消費者模式之後,生產者和消費者可以是兩個獨立的併發主體。
生產者把製造出來的資料往緩衝區一丟,就可以再去生產下一個資料。基本上不用依賴消費者的處理速度。郵寄快遞的人直接把快遞扔個快遞中心之後就不用管了。
快取:生產者消費者速度不匹配,暫存資料。如果郵寄快遞的人一次要郵寄多個快遞,那麼郵寄員無法郵寄,就可以把其他的快遞暫存在快遞中心。也就是生產者短時間內生產資料過快,消費者來不及消費,未處理的資料可以暫時存在緩衝區中。
二、Go語言實現
單向channel
最典型的應用是“生產者消費者模型”。channel
又分為有緩衝和無緩衝channel
。channel
中引數傳遞的時候,是作為引用傳遞。
1、無緩衝channel
示例程式碼一實現如下
package main
import "fmt"
func producer(out chan <- int) {
for i:=0; i<10; i++{
data := i*i
fmt.Println("生產者生產資料:", data)
out <- data // 緩衝區寫入資料
}
close(out) //寫完關閉管道
}
func consumer(in <- chan int){
// 同樣讀取管道
//for{
// val, ok := <- in
// if ok {
// fmt.Println("消費者拿到資料:", data)
// }else{
// fmt.Println("無資料")
// break
// }
//}
// 無需同步機制,先做後做
// 沒有資料就阻塞等
for data := range in {
fmt.Println("消費者得到資料:", data)
}
}
func main(){
// 傳參的時候顯式型別像隱式型別轉換,雙向管道向單向管道轉換
ch := make(chan int) //無緩衝channel
go producer(ch) // 子go程作為生產者
consumer(ch) // 主go程作為消費者
}
這裡使用無緩衝channel
,生產者生產一次資料放入channel
,然後消費者從channel
讀取資料,如果沒有隻能等待,也就是阻塞,直到管道被關閉。所以巨集觀是生產者消費者同步執行。
另外:這裡是只而外開闢一個go程執行生產者,主go程執行消費者,如果也是用一個新的go程執行消費者,就需要阻塞main函式中的go程,否則不等待消費者和生產者執行完畢,主go程退出,程式直接結束,如示例程式碼三。
生產者每一次生產,消費者也只能拿到一次資料,緩衝區作用不大。結果如下:
2、有緩衝channel
示例程式碼二如下
package main
import "fmt"
func producer(out chan <- int) {
for i:=0; i<10; i++{
data := i*i
fmt.Println("生產者生產資料:", data)
out <- data // 緩衝區寫入資料
}
close(out) //寫完關閉管道
}
func consumer(in <- chan int){
// 無需同步機制,先做後做
// 沒有資料就阻塞等
for data := range in {
fmt.Println("消費者得到資料:", data)
}
}
func main(){
// 傳參的時候顯式型別像隱式型別轉換,雙向管道向單向管道轉換
ch := make(chan int, 5) // 新增緩衝區,5
go producer(ch) // 子go程作為生產者
consumer(ch) // 主go程作為消費者
}
有緩衝channel
,只修改ch := make(chan int, 5)
// 新增緩衝一句,只要緩衝區不滿,生產者可以持續向緩衝區channel
放入資料,只要緩衝區不為空,消費者可以持續從channel
讀取資料。就有了非同步,併發的特性。
結果如下:
這裡之所以終端生產者連續列印了大於緩衝區容量的資料,是因為終端列印屬於系統呼叫也是有延遲的,IO操作的時候,生產者同時向管道寫入,請求列印,管道的寫入讀取與終端輸出列印速度不匹配。
三、實際應用
實際應用中,同時訪問同一個公共區域,同時進行不同的操作。都可以劃分為生產者消費者模型,比如訂單系統。
很多使用者的訂單下達之後,放入緩衝區或者佇列中,然後系統從緩衝區中去讀來真正處理。系統不必開闢多個執行緒來對應處理多個訂單,減少系統併發的負擔。通過生產者消費者模式,將訂單系統與倉庫管理系統隔離開,且使用者可以隨時下單(生產資料)。
如果訂單系統直接呼叫倉庫系統,那麼使用者單擊下訂單按鈕後,要等到倉庫系統的結果返回。這樣速度會很慢。
也就是:使用者變成了生產者,處理訂單管理系統變成了消費者。
程式碼示例三如下
package main
import (
"fmt"
"time"
)
// 模擬訂單物件
type OrderInfo struct {
id int
}
// 生產訂單--生產者
func producerOrder(out chan <- OrderInfo) {
// 業務生成訂單
for i:=0; i<10; i++{
order := OrderInfo{id: i+1}
fmt.Println("生成訂單,訂單ID為:", order.id)
out <- order // 寫入channel
}
// 如果不關閉,消費者就會一直阻塞,等待讀
close(out) // 訂單生成完畢,關閉channel
}
// 處理訂單--消費者
func consumerOrder(in <- chan OrderInfo) {
// 從channel讀取訂單,並處理
for order := range in{
fmt.Println("讀取訂單,訂單ID為:", order.id)
}
}
func main() {
ch := make(chan OrderInfo, 5)
go producerOrder(ch)
go consumerOrder(ch)
time.Sleep(time.Second * 2)
}
這裡如上面邏輯類似,不同的是用一個,OrderInfo
結構體模擬訂單作為業務處理物件。主執行緒使用time.Sleep(time.Second * 2)
阻塞,否則,程式立即停止。
結果如下:
到此這篇關於Go語言實現一個簡單生產者消費者模型的文章就介紹到這了
本作品採用《CC 協議》,轉載必須註明作者和本文連結