大家好,夜鶯項目發佈 v6.4.0 版本,新增全局巨集變數功能,本文為大家簡要介紹一下相關更新內容。 全局巨集變數功能 像 SMTP 的配置中密碼類型的信息,之前都是以明文的方式在頁面展示,夜鶯支持全局巨集變數之後,可以在變數管理配置一個 smtp_password 的密碼類型的變數,在 SMTP 配置頁 ...
協程與線程
線程在創建、切換、銷毀時候,需要消耗CPU的資源。
協程就是將一段程式的運行狀態打包, 可以線上程之間調度。減少CPU在操作線程的消耗
協程、線程、進程 這塊網上非常多文章講了,就不多敘述了。
歸納下:
進程用分配記憶體空間
線程用來分配CPU時間
協程用來精細利用線程
協程的本質是一段包含了運行狀態的程式 後面介紹後,會對這個概念更好理解
協程的本質
上面講了 ,協程的本質就是 一段程式的運行狀態的打包:
func Do() {
for i := 1; i <= 1000; i++ {
fmt.Println(i)
time.Sleep(time.Second)
}
}
func main() {
go Do()
select {}
}
例如上面這段代碼,開了一個協程,然後一直迴圈列印。
假設程式都還有很多其他的協程也在工作,發現這個協程工作太久了,系統會進行切換別的協程,現在這個協程會放入協程隊列中。
問題:要做到這點,協程需要怎麼保存這個執行狀態?
- 需要一個函數的調用棧,記錄執行了那些函數,(例子中只有一個,正常情況下會是很多函數相互調用) 函數執行完後,還需要回到上層函數,所以要保存函數棧信息。
- 需要記錄當前執行到了 那行代碼,不能把多執行,也不能少執行那句代碼,不然程式會不可控。
- 需要一個空間,存儲整個協程的數據,例如變數的值等。
協程的底層定義
在runtime的runtim2.go中
type g struct {
// 只留了少量幾個,裡面有非常多的欄位。
stack stack // 調用棧
m *m // 協程關聯了一個m (GMP)
sched gobuf // 協程的現場
goid uint64 // 協程的編號
atomicstatus atomic.Uint32 // 協程的狀態
}
type gobuf struct {
sp uintptr // 當前調用的函數
pc uintptr // 執行語句的指針
g guintptr
ctxt unsafe.Pointer
ret uintptr
lr uintptr
bp uintptr // for framepointer-enabled architectures
}
// 棧的定義
type stack struct {
lo uintptr // 低地址
hi uintptr // 高地址
}
整體下:
假如有這麼一段代碼:
func do3() {
fmt.Println("dododo")
}
func do2() {
do3()
}
func do1() {
do2()
}
func main() {
go do1()
time.Sleep(time.Hour)
}
在do2斷點:
能看到下方的調用棧中,會自動插入一個 goexit 在棧頭。
小結下,整體的結構如下:
總結:
runtime 中,協程的本質是一個g 結構體
stack:堆棧地址
gobuf:目前程式運行現場
atomicstatus: 協程狀態
線程的底層 m
操作系統的線程是由操作系統管理,這裡的m只是記錄線程的信息。
截取部分代碼:
type m struct {
g0 *g // goroutine with scheduling stack
id int64 // id號
morebuf gobuf // gobuf arg to morestack
curg *g // 當前運行的g
p puintptr // attached p for executing go code (nil if not executing go code)
mOS // 系統線程信息
}
go 是go程式啟動創建的第一個協程,用來操控調度器的,第二個是主協程,可以看下 go啟動那篇
小結:
runtime 中將操作系統線程抽象為 m結構體
g0:g0協程,操作調度器
curg:current g,目前線程運行的g
mOs:操作系統線程信息
如何工作
協程究竟是如何在 線程中工作的 ?
先講總結,然後跟著總結往下看:
這是單個線程的迴圈,沒有P的存在。
1. schedule() 是線程獲取 協程的入口方法
線程通過執行 g0協程棧,獲取 待執行的 協程
也就是意味著,每次線程執行 這個schedule
方法,就意味著會切換一個 協程。
這個結論很重要,後面 協程調度時候,會大量看到調用這個方法。
在runtime的 proc.go下麵能看到這個方法,這裡只留了兩行代碼,
只和目前邏輯相關的,這個方法後面還要多次讀
func schedule() {
gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available
execute(gp, inheritTime)
}
這裡的gp就是 待執行的g
可以和上面的圖對上,這裡去 `Runnable` 找一個協程。然後,調用 `execute` 方法。
至於怎麼去找的,知道GMP的肯定都知道,這個後面聊。
也只有部分代碼,和這裡業務相關的
func execute(gp *g, inheritTime bool) {
mp := getg().m //獲取m,線程的抽象
mp.curg = gp // 還記得 m的定義 裡面有個 當前的 g 在這裡賦值了
gp.m = mp // g的定義也有個 m,這裡也賦值了
gogo(&gp.sched)
}
到gogo
func gogo(buf *gobuf) // 只有定義,說明是彙編實現的,而且是平臺相關的
// func gogo(buf *gobuf)
// 這裡把 g的gobuf傳過去了,gobuf 存著 sp 和 pc ,當前的執行函數,和執行語句
// 到這裡就基本對應上了
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $0-8
MOVQ buf+0(FP), BX // gobuf
MOVQ gobuf_g(BX), DX
MOVQ 0(DX), CX // make sure g != nil
JMP gogo<>(SB)
// 插入了 goexit的棧針 然後開始運行業務
TEXT gogo<>(SB), NOSPLIT, $0
get_tls(CX)
MOVQ DX, g(CX)
MOVQ DX, R14 // set the g register
MOVQ gobuf_sp(BX), SP // restore SP 插入了 goexit的棧針
MOVQ gobuf_ret(BX), AX
MOVQ gobuf_ctxt(BX), DX
MOVQ gobuf_bp(BX), BP
MOVQ $0, gobuf_sp(BX) // clear to help garbage collector
MOVQ $0, gobuf_ret(BX)
MOVQ $0, gobuf_ctxt(BX)
MOVQ $0, gobuf_bp(BX)
MOVQ gobuf_pc(BX), BX
JMP BX
在運行業務之前 jmp bx
,都還在 g0的協程棧上。
目前,已經把開始執行,到執行都整理了一遍,但是,沒有講 goexit
插入 到底有什麼作用?
經驗豐富的伙伴大致能猜到, 當執行完了協程的任務後,需要回到
schedule
方法中, 線程重新去執行別的協程,這就是goexit
的作用
goexit
彙編實現
TEXT runtime·goexit(SB),NOSPLIT|TOPFRAME,$0-0
BYTE $0x90 // NOP
CALL runtime·goexit1(SB) // 去調用 goexit1 這個方法
// Finishes execution of the current goroutine.
func goexit1() {
mcall(goexit0) // 通過mcall 調用goexit0
}
// mcall switches from the g to the g0 stack and invokes fn(g),
// 切換到 g0 棧
func mcall(fn func(*g))
就是只,上面的都是在 業務協程中,運行的,到這裡,開始使用 g0棧去運行,goexit0
// goexit continuation on g0.
func goexit0(gp *g) {
mp := getg().m
pp := mp.p.ptr()
casgstatus(gp, _Grunning, _Gdead)
gcController.addScannableStack(pp, -int64(gp.stack.hi-gp.stack.lo))
if isSystemGoroutine(gp, false) {
sched.ngsys.Add(-1)
}
gp.m = nil
locked := gp.lockedm != 0
gp.lockedm = 0
mp.lockedg = 0
gp.preemptStop = false
gp.paniconfault = false
gp._defer = nil // should be true already but just in case.
gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.
gp.writebuf = nil
gp.waitreason = waitReasonZero
gp.param = nil
gp.labels = nil
gp.timer = nil
schedule()
}
// 對結束的g進行了一些置0的工作,然後調用了 schedule()
schedule()
意味著 為現在的線程,切換協程。
到此,和上面的圖都對應上了。但是目前還是單線程,多線程時候,是如何工作了,下篇再聊。