阻塞?
在Go語言中,阻塞通常指的是一個goroutine(輕量級線程)在等待另一個goroutine完成操作(如I/O操作、channel通信等)時,暫時停止執行的現象。Go語言提供了多種同步和通信機制,可以用于實現阻塞的效果。
使用 Channel 實現阻塞
Channel 是Go語言中的一個核心特性,用于在goroutines之間進行通信。通過channel,你可以實現阻塞等待數據或命令。
package mainimport ("fmt""time"
)func main() {c := make(chan struct{})go func() {fmt.Println("業務處理~~~")time.Sleep(2 * time.Second)fmt.Println("業務處理完成~~~")close(c) // 關閉channel,通知工作完成}()<-c // 阻塞等待channel關閉fmt.Println("處理其他業務~~~")
}
使用 WaitGroup 實現阻塞
WaitGroup 是Go語言中用于同步一組并發操作的另一個工具。它通過計數器來跟蹤完成的操作數量。
package mainimport ("fmt""strconv""sync""time"
)func main() {var wg sync.WaitGroup //控制并發組doWork := func(i int) {// wg.Done(): 表示一個事件已經完成。它等價于 wg.Add(-1),但更明確地表達了“完成一個任務”的意圖,并且在使用上更安全,因為它不會導致計數變為負數(如果已經到達零,則會panic)。defer wg.Done() // 當函數返回時,通知WaitGroup一個操作已完成相當于wg.Add(-1)fmt.Println("處理業務~~~" + strconv.Itoa(i))time.Sleep(2 * time.Second)fmt.Println("業務處理完成~~~" + strconv.Itoa(i))}for i := 0; i < 5; i++ {wg.Add(1) // 增加WaitGroup的計數器go doWork(i) // 啟動一個goroutine做工作}//主goroutine調用wg.Wait(),直到所有啟動的goroutines都通過調用wg.Done()通知它們已經完成工作wg.Wait() // 阻塞,直到WaitGroup的計數器為0fmt.Println("所有業務處理完成~~~")
}
使用 Mutex 和 Conditional Variables 實現阻塞
Mutex(互斥鎖)和條件變量可以用來同步訪問共享資源,并實現基于條件的阻塞。?
package mainimport ("fmt""sync""time"
)func main() {var mtx sync.Mutex //創建互斥鎖cond := sync.NewCond(&mtx) //使用mtx作為底層互斥鎖ready := false// 啟動一個 goroutine 來改變條件變量 ready 的值,并通知 cond。go func() {fmt.Println("循環跟goroutine是go內部決定先調度的--------------------goroutine--------------------")time.Sleep(3 * time.Second)mtx.Lock() //使用互斥鎖ready = truecond.Signal() // 喚醒至少一個等待的 goroutinemtx.Unlock() //解鎖}()mtx.Lock() // 鎖定互斥鎖,準備進入條件等待for !ready {fmt.Println("循環跟goroutine是go內部決定先調度的--------------------阻塞--------------------")cond.Wait() // 阻塞,直到 cond.Signal() 被調用//mtx.Unlock()}mtx.Unlock() // 解鎖互斥鎖,繼續執行(此處mtx.Unlock()在for循環里面阻塞等待完成后也可以,也可以沒有,因為主線程會結束,但如果后續還需要獲取互斥鎖則必須要釋放否則報錯)fmt.Println("準備繼續~~~")
}
這里是一些關鍵的修改和注意事項:
-
sync.Cond
的使用需要一個sync.Mutex
作為其底層的互斥鎖。在使用cond.Wait()
之前,必須先鎖定這個互斥鎖。 -
在
cond.Wait()
調用中,當前的互斥鎖會被自動釋放,goroutine 會阻塞直到它被cond.Signal()
或cond.Broadcast()
喚醒。 -
一旦
cond.Wait()
返回,goroutine 會重新獲取互斥鎖,然后繼續執行循環或代碼塊。 -
在
cond.Signal()
調用之后,您需要在某個地方調用mtx.Unlock()
來釋放互斥鎖,否則主 goroutine 會在cond.Wait()
之后無法獲取到鎖。 -
您的代碼中,
cond.Wait()
之后的mtx.Unlock()
應該在for
循環之外,以避免在循環的每次迭代中重復加鎖和解鎖。?
在Go語言中,
sync.Mutex
(互斥鎖)用于保護共享資源不被多個goroutine同時修改,以避免競態條件。sync.Cond
(條件變量)與互斥鎖結合使用,可以在多個goroutine之間同步共享條件。以下是關于何時使用mtx.Lock()
和mtx.Unlock()
的指導:
mtx.Lock()
- 在訪問或修改由互斥鎖保護的共享資源之前使用。
- 在調用?
cond.Wait()
?之前使用,以確保在等待條件變量時,共享資源不會被其他goroutine并發訪問。 - 在調用?
cond.Signal()
?或?cond.Broadcast()
?之前使用,因為這些操作需要在互斥鎖保護的臨界區內執行。
mtx.Unlock()
- 在完成對共享資源的訪問或修改后使用。
- 在?
cond.Wait()
?返回后使用,因為我們已經完成了等待期間需要的共享資源訪問,并且需要重新獲取互斥鎖以繼續執行。 - 在不再需要互斥鎖保護當前goroutine的執行路徑時使用,以允許其他等待互斥鎖的goroutine繼續執行。
注意事項
- 互斥鎖必須在獲取后及時釋放,否則會導致死鎖。
- 通常,獲取互斥鎖和釋放互斥鎖成對出現,以避免忘記釋放鎖。
永久阻塞
Go 的運行時的當前設計,假定程序員自己負責檢測何時終止一個?
goroutine
?以及何時終止該程序。可以通過調用?os.Exit
?或從?main()
?函數的返回來以正常方式終止程序。而有時候我們需要的是使程序阻塞在這一行。
使用 sync.WaitGroup?
一直等待直到?WaitGroup
?等于 0?
package mainimport "sync"func main() {var wg sync.WaitGroupwg.Add(1)wg.Wait()
}
空 select
?select{}
是一個沒有任何?case
?的?select
,它會一直阻塞
package mainfunc main() {select{}
}
?死循環
雖然能阻塞,但會 100%占用一個 cpu。不建議使用
package mainfunc main() {for {}
}
?用 sync.Mutex
一個已經鎖了的鎖,再鎖一次會一直阻塞,這個不建議使用
package mainimport "sync"func main() {var m sync.Mutexm.Lock()
}
?os.Signal
系統信號量,在 go 里面也是個?channel
,在收到特定的消息之前一直阻塞??
package mainimport ("os""os/signal""syscall"
)func main() {sig := make(chan os.Signal, 2)//syscall.SIGTERM 是默認的終止進程信號,通常由服務管理器(如systemd、supervisor等)發送來請求程序正常終止。//syscall.SIGINT 是中斷信號,一般由用戶按下Ctrl+C鍵觸發,用于請求程序中斷執行signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)<-sig
}
從終端發送信號
-
Ctrl+C: 在大多數Unix-like系統(包括Linux和macOS)以及Windows的命令行中,按
Ctrl+C
鍵會向當前前臺進程發送一個SIGINT
(中斷)信號。這通常是停止Go程序的快捷方式。 -
Kill命令: 如果你的程序在后臺運行,并且你知道其進程ID(PID),可以通過終端發送一個信號。例如,發送一個
SIGTERM
信號,可以使用:kill PID或者指定型號類型kill -SIGTERM PID
?從Go代碼內部發送信號
package mainimport ("os""os/signal""syscall""time"
)func main() {sig := make(chan os.Signal, 2)//syscall.SIGTERM 是默認的終止進程信號,通常由服務管理器(如systemd、supervisor等)發送來請求程序正常終止。//syscall.SIGINT 是中斷信號,一般由用戶按下Ctrl+C鍵觸發,用于請求程序中斷執行signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)go func() {time.Sleep(10 * time.Second)sig <- syscall.SIGTERM}()go func() {time.Sleep(5 * time.Second)sig <- syscall.SIGINT}()<-sig
}
使用外部工具或服務管理器
如果你的Go程序作為服務運行,可能由如systemd、supervisord等服務管理器控制,這些管理器通常提供了發送信號給托管服務的機制。具體操作需參考相應服務管理器的文檔。
空 channel 或者 nil channel?
channel
?會一直阻塞直到收到消息,nil channel
?永遠阻塞。?
package mainfunc main() {c := make(chan struct{})<-c
}
package mainfunc main() {var c chan struct{} //nil channel<-c
}
?總結
?注意上面寫的的代碼大部分不能直接運行,都會?panic
,提示“all goroutines are asleep - deadlock!”,因為 go 的?runtime
?會檢查你所有的?goroutine
?都卡住了, 沒有一個要執行。
你可以在阻塞代碼前面加上一個或多個你自己業務邏輯的?goroutine
,這樣就不會?deadlock
?了。
。
?