CSP(Communicating Sequential Processes) 通訊順序程式。用於描述併發通訊中的模式。golang原生的chan通道可阻塞,可鎖可佇列性。它的目的在於,保證併發執行的可控,即讓併發的無序執行在某些條件下具備有序可控。
Goroutine本身是非同步執行的程式碼塊,它的執行順序與其書寫分配 任務順序無關,與執行時通道,排程策略相關。
生產者 VS 消費者
通道阻塞與否事關後續程式碼的執行時機。通常同一個函式或goroutine內程式碼塊是同步順序執行的。可一旦內有通道阻塞,則意味著當前阻塞點後續程式碼的執行會在其他goroutine某段程式碼之後釋放(即依賴於另一個對應通道讀寫),換而言之,會發生執行上下文切換,而並非上一行阻塞點執行完畢,立刻執行下一行。表現出來的樣子,有小子在阻塞點位前插隊。
細思極恐,若不停的切換上下文,則存在多個goroutine中有程式碼段被阻塞的通道切割成了多個小片段,每次切換實際上只是執行區間小片段,然後再進入其它的Groutine。這有點更像goto,執行一段跳到另外一段,未來某時又跳回來執行後續程式碼。
chan 本質是有序,當它處於阻塞時,是指該程式碼點之後的程式碼執行,需要等待執行時觸發其狀態變更為非阻塞時方可執行。下述布林變數b在,done通道讀取之後時。其列印結果完全不同,決定了其布林值是輸出頭或尾。
package main
import (
"flag"
"fmt"
"log"
"os"
"runtime"
"runtime/pprof"
)
type Consumer struct {
msgs *chan int
}
func NewConsumer(msgs *chan int) *Consumer {
return &Consumer{msgs: msgs}
}
// consumer 不停地讀取通道
func (c *Consumer) consume(){
fmt.Println("consume: Started")
for {
msg := <-*c.msgs
fmt.Println("consume: Received:", msg)
}
}
// Producer 結構體
type Producer struct {
msgs *chan int
done *chan bool
}
func NewProducer(msgs *chan int, done *chan bool)*Producer {
return &Producer{msgs: msgs, done:done}
}
// 生產者方法
func (p *Producer)produce(max int){
fmt.Println("produce: Started")
for i := 0; i < max; i++ {
fmt.Println("produce:Sending ", i)
*p.msgs <-i
}
// 阻塞在外部得到訊號退出
*p.done <- true
fmt.Println("produce: Done")
}
func main() {
cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file")
memprofile := flag.String("memprofile", "", "write memory profile to `file`")
max := flag.Int("n", 5, "defines the number of messages")
flag.Parse()
runtime.GOMAXPROCS(runtime.NumCPU())
if *cpuprofile != ""{
f,err := os.Create(*cpuprofile)
if err != nil{
log.Fatal("could not create CPU profile: ", err)
}
if err := pprof.StartCPUProfile(f);err!=nil{
log.Fatal("could not start CPU profile: ", err)
}
defer pprof.StopCPUProfile()
}
var msgs = make(chan int)
var done = make(chan bool)
// 使用結構體方法作為goroutine, 同一結構體資源使用必使用鎖(即chan),即使訪問具有有效性
go NewProducer(&msgs,&done).produce(*max)
go NewConsumer(&msgs).consume()
var b bool
fmt.Println(b)
<-done
if *memprofile != ""{
f,err := os.Create(*memprofile)
if err!= nil{
log.Fatal("could not create memory profile: ", err)
}
runtime.GC()
if err := pprof.WriteHeapProfile(f);err!=nil{
log.Fatal("could not write memory profile: ", err)
}
f.Close()
}
}
效果
D:\code-base\gomod\gott>go run "d:\code-base\gomod\gott\main.go"
false
consume: Started
produce: Started
produce:Sending 0
produce:Sending 1
consume: Received: 0
consume: Received: 1
produce:Sending 2
produce:Sending 3
consume: Received: 2
consume: Received: 3
produce:Sending 4
produce: Done
D:\code-base\gomod\gott>
死迴圈中的通道
之所以對通道說這麼多,是想講明不像通常所見,從上到下書寫執行。而是強調在goroutine中阻塞,會迅速切換上下文,並不會陷入consume方法看起來死迴圈(若無通道又非goroutine,該段程式碼是鐵定在理論上會耗盡cpu計算資源)。另外補充一下,每個goroutine的時間片是有時限。通道在某種意義上協調多個goroutine之間的執行
從另一方面也說明並非讀寫通道就會切換執行上下文,比如在無緩衝寫通道不會阻塞後續程式碼,有多次寫入未滿緩衝通道亦不會阻塞。所以無論是有緩衝通道,還是無緩衝通道,對於goroutine而言他上下文切換,通道阻塞是其觸發條件之一。
理髮師問題
理髮店在理髮室內有一張理髮椅,且在待理室有若干個供客戶等候休息的凳子。當完成理髮,理髮師不再伺候,直接進待理室看看是否還有其他待理髮的人,若無則返回理髮室睡覺。每當有客人進店,他會先瞅瞅理髮師在作什麼。若在睡則叫他起來幹活,若在理髮則在待理室等候,如果沒有凳子可供休息,則會選擇直接離開。
package main
import (
"fmt"
"sync"
"time"
)
const(
sleeping = iota
checking
cutting
)
var stateLog = map[int]string{
0:"Sleeping",
1:"Checking",
2:"Cutting",
}
var wg *sync.WaitGroup // 潛在的客戶數
type Barber struct {
name string
sync.Mutex
state int // Sleeping/Checking/Cutting
customer *Customer
}
type Customer struct {
name string
}
// 獲取當前客戶地址
func (c *Customer)String() string{
return fmt.Sprintf("%p", c)[7:]
}
func NewBarber()(b *Barber){
return &Barber{
name: "Sam",
state:sleeping,
}
}
// 理髮師 goroutine
// 核查客戶
// 理髮師睡覺 等待喚醒
func barber(b *Barber, wr chan *Customer,wakers chan *Customer){
for {
b.Lock()
defer b.Unlock()
b.state = checking
b.customer=nil
// 檢查待理室客戶
fmt.Printf("Checking waiting room: %d\n", len(wr))
// 模擬查詢時阻塞
time.Sleep(time.Microsecond*100)
// 待理室分配任務
select{
case c := <-wr:
// 1.主動理髮
// 理髮
HairCut(c,b)
// 解鎖
b.Unlock()
default:
// 2. 喚醒理髮
// 待理室人員為空,列印理髮師名
fmt.Printf("Sleeping Barber - %s \n", b.customer)
// 重置理髮狀態
b.state = sleeping
b.customer = nil
b.Unlock()
// 喚醒者客戶出現
c := <-wakers
// 理髮時加鎖
b.Lock()
fmt.Printf("Woken by %s\n", c)
HairCut(c,b)
b.Unlock()
}
}
}
// 理髮邏輯,此段程式碼多個goroutine共用,注意加解鎖
func HairCut(c *Customer, b *Barber){
b.state = cutting
b.customer = c
b.Unlock()
fmt.Printf("Cutting %s hair\n", c)
// 模擬理髮時間
time.Sleep(time.Millisecond * 100)
b.Lock()
wg.Done()
// 使用者走人
b.customer = nil
}
// 客戶 goroutine
// 若待理室已滿,則理髮失敗,否則進來的都是客戶
func customer(c *Customer, b *Barber, wr chan<- *Customer, wakers chan<- *Customer){
// 客戶進來
time.Sleep(time.Microsecond*50)
// 理髮上鎖
b.Lock()
// 當前使用者,理髮師狀態,待理室使用者數,具備喚醒權的使用者婁數,喚醒人
fmt.Printf(" Customer %s checks %s barber | room: %d, w %d - customer: %s\n", c, stateLog[b.state],len(wr),len(wakers),b.customer)
switch b.state {
// 理髮師在睡覺
case sleeping:
select {
// 客戶成為待喚醒者
case wakers <- c:
default:
// 否則先成為待理室成員
select{
case wr <-c:
default:
// 待理室沒人,則預設理髮師完成工作
wg.Done()
}
}
case cutting:
select{
// 待理室有人
case wr <-c:
default:
// 待理室已滿坐不下了,預設執行此多餘人離開理髮店
wg.Done()
}
case checking:
// 丟擲 客戶goroutine 不應核驗發師,應由理髮師核檢待理室的客戶
panic("Customer shouldn't check for the Barber when Barber is Checking the waiting room")
}
b.Unlock()
}
func main() {
b := NewBarber()
b.name = "Pardon"
// 模擬待理室有5把椅子
WaitingRoom := make(chan *Customer, 5)
// 存在一位喚醒者
Wakers := make(chan *Customer,1)
// 理髮師處理
go barber(b, WaitingRoom, Wakers)
time.Sleep(time.Microsecond*100)
wg = new(sync.WaitGroup)
n := 10
// 增加10個計數點
wg.Add(10)
// 生成10個客戶
for i := 0; i < n; i++ {
time.Sleep(time.Microsecond*50)
c := new(Customer)
// 分配十個消費goroutine
go customer(c, b, WaitingRoom, Wakers)
}
// 阻塞主執行緒
wg.Wait()
fmt.Println("No more customers for the day")
}
效果
D:\code-base\gomod\gott>go run "d:\code-base\gomod\gott\main.go"
Checking waiting room: 0
Sleeping Barber - <nil>
Customer 32010 checks Sleeping barber | room: 0, w 0 - customer: <nil>
Woken by 32010
Cutting 32010 hair
Customer 32020 checks Cutting barber | room: 0, w 0 - customer: 32010
Customer 4c1f0 checks Cutting barber | room: 1, w 0 - customer: 32010
Customer 32040 checks Cutting barber | room: 2, w 0 - customer: 32010
Customer 32050 checks Cutting barber | room: 3, w 0 - customer: 32010
Customer 32060 checks Cutting barber | room: 4, w 0 - customer: 32010
Customer 4c220 checks Cutting barber | room: 5, w 0 - customer: 32010
Customer 4c230 checks Cutting barber | room: 5, w 0 - customer: 32010
Customer e2000 checks Cutting barber | room: 5, w 0 - customer: 32010
Customer c0060 checks Cutting barber | room: 5, w 0 - customer: 32010
Checking waiting room: 5
Cutting 32020 hair
Checking waiting room: 4
Cutting 4c1f0 hair
Checking waiting room: 3
Cutting 32040 hair
Checking waiting room: 2
Cutting 32050 hair
Checking waiting room: 1
Cutting 32060 hair
Checking waiting room: 0
No more customers for the day
D:\code-base\gomod\gott>cls
小結
前者基於結構體的方法,後者直接用函式作為gortouine。二者在訪問共用邏輯時注意加鎖,多個goroutine執行通過chan來協作。
本作品採用《CC 協議》,轉載必須註明作者和本文連結