Sun公司提供了JavaMail用來實現郵件發送,但是配置煩瑣,Spring中提供了JavaMailSender用來簡化郵件配置,Spring Boot則提供了MailSenderAutoConfiguration對郵件的發送做了進一步簡化。 v準備工作 開通POP3/SMTP服務或者IMAP/SM ...
當m在執行某個g的時候,g非常耗時,例如一個for迴圈,每次迴圈sleep1分鐘,迴圈1000次。
這個例子看似無聊,卻是很難解決的,成功的避開了2個系統切換時機。
如果這個時候,一直執行這個g,別的g就會得不到執行,例如有g是處理用戶支付的,這樣就會造成收錢不積極。
協程饑餓問題
本地隊列
本地隊列因為 某個G一直 占著M,導致其他G無法執行。
如果占用時間過長的這個G,能讓出來M,讓別的G也能執行,本地隊列迴圈的著執行,就能解決這個問題。
全局隊列
除了本地隊列,全局隊列也會有這個問題,如果一個新創建的g,放在全局隊列中,而現有的p的本地隊列都未執行完,則全局隊列需要排隊很久。
解決辦法,每過一段時間,每個本地隊列都先來全局隊列中取1個,這樣就能解決這個問題。
代碼實現:
又到了findRunnable()
// Check the global runnable queue once in a while to ensure fairness.
if pp.schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp := globrunqget(pp, 1)
unlock(&sched.lock)
if gp != nil {
return gp, false, false
}
}
這個優先順序在 本地隊列之前。之前看過 globrunqget()
中的邏輯,當max為1時候,就只會取一個。
每61次,就去全局隊列中拿一個。
解決辦法
協程因為獨特的數據結構,能能夠暫停的,之前協程的本質有介紹過,暫停後,讓別的g也開始迴圈執行。
切換時機
主動掛起
業務方法主動調用gopark
然後,切換協程。
源碼在proc.go中
// Puts the current goroutine into a waiting state and calls unlockf on the
// 讓當前的 g 進入 waiting的狀態
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceReason traceBlockReason, traceskip int) {
if reason != waitReasonSleep {
checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
}
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
if status != _Grunning && status != _Gscanrunning {
throw("gopark: bad g status")
}
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason
mp.waitTraceBlockReason = traceReason
mp.waitTraceSkip = traceskip
releasem(mp)
// can't do anything that might move the G between Ms here.
mcall(park_m) // 切換到了 g0棧,前面講過mcall
}
// park continuation on g0.
func park_m(gp *g) {
mp := getg().m
// 中間還有代碼
if fn := mp.waitunlockf; fn != nil {
ok := fn(gp, mp.waitlock)
mp.waitunlockf = nil
mp.waitlock = nil
if !ok {
if traceEnabled() {
traceGoUnpark(gp, 2)
}
casgstatus(gp, _Gwaiting, _Grunnable)
execute(gp, true) // Schedule it back, never returns.
}
}
schedule() //調了這個方法,之前講過,一旦調用這個方法,就會給m找新的g
}
有個問題:gapark是小寫的,程式員在編碼中是使用不了的,那怎麼讓業務主動調用?
系統runtime裡面很多方法有去調用,例如
time.Sleep、channel的等待等
系統調用完成時
在進行一些系統調用後,例如網路請求等會主動去調這個 exitsyscall()
這個方法,這個方法也會最終走到 schedule()
這個源碼在 syscall_aix.go中,因為有好幾層函數調用,就不貼出來了。
標記搶占 基於 morestack
1. 系統監控到 Goroutine 運行超過 10ms
2. 將 g.stackguard0 置為 Oxfffffade
morestack() 方法,在函數跳轉時候,會自動調用,本意是檢測下新的函數,有沒有足夠的棧空間。
系統在這個函數中,去做了一部分協程切換的,防止一些 耗時比較久的協程,不去觸發上面兩種方案。
看下源碼:
// Called during function prolog when more stack is needed.
// record an argument size. For that purpose, it has no arguments.
TEXT runtime·morestack(SB),NOSPLIT,$0-0
// 中間很多擴充棧空間的代碼
CALL runtime·newstack(SB) // 跟進這個方法
CALL runtime·abort(SB) // crash if newstack returns
RET
// Goroutine preemption request.
// 0xfffffade in hex.
stackPreempt = uintptrMask & -1314
func newstack() {
// 中間還有很多源碼
// 搶占標記 如果g的.stackguard0 欄位被標記為搶占,就會觸發下麵的邏輯
stackguard0 := atomic.Loaduintptr(&gp.stackguard0)
preempt := stackguard0 == stackPreempt
if preempt {
// Act like goroutine called runtime.Gosched.
gopreempt_m(gp) // never return
}
}
func gopreempt_m(gp *g) {
if traceEnabled() {
traceGoPreempt()
}
goschedImpl(gp)
}
func goschedImpl(gp *g) {
//刪了一些源碼,最終調了 schedule
schedule()
}
基於信號的搶占標記
開頭那個 for 迴圈的例子,雖然很無聊,但是去避開了上面那種方案,
1. 不會調用 gopark
2. 不會系統調用
3.不會調用 morestack,因為沒有函數調用
這時候,可以使用 信號 來觸發協程的切換。
信號量可以在多線程和多進程直接進行通信(管道、共用記憶體、信號、消息隊列一般作為多進程通信方式)。
原理:
操作系統中,有很多基於信號的底層通信方式,例如: SIGPIPE / SIGURG / SIGHUP
線程可以註冊對應信號的處理函數
go的實現流程:
註冊 `SIGURG`信號的處理函數
`GC`工作時,向目標線程發送信號
線程收到信號,觸發調度,`gc`發送 `sigurg` 觸發`runtime`的 `doSigPreempt()`
能夠猜到 doSigPreempt()
, 最終會去調 schedule()
方法。 源碼就不貼了。
開發中,協程過多的問題
1. 文件打開數限制
過多協程調用文件讀寫,會操作系統崩潰。
2. 記憶體限制
過多協程創建,達到了記憶體的限制
3. 調度開銷過大
過多協程,導致調度器調度複雜度增大
解決辦法:
1. 優化業務邏輯
2. 利用 channel 的緩存區
3. 協程池
4. 調整系統資源
2和3都是從控制協程的數量入手,2適合 單個業務場景,3適合全局。
1和4好理解
利用 channel 的緩存區
func do(c chan interface{}) {
fmt.Println("do it")
<-c
}
func main() {
// 利用channel的特性來控制 go協程的個數
ch := make(chan interface{}, 100)
for {
ch <- struct{}{}
go do(ch)
}
}
這種適合在一個場景下適用,不推薦全局使用。
協程池
代表:https://github.com/Jeffail/tunny
原理:類似某些語言的線程池
1.預創建一定數量的協程
2.將任務送入協程池隊列
3.協程池不斷取出可用協程,執行任務
慎用協程池
Go語言的線程,已經相當於池化了
二級池化會增加系統複雜度
Go語言的初衷是希望協程即用即毀,不要池化
到此,go的GMP完結。