go協程、線程的本質,如何協調運作

来源:https://www.cnblogs.com/studyios/archive/2023/11/30/17866765.html
-Advertisement-
Play Games

大家好,夜鶯項目發佈 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 {}
  }

例如上面這段代碼,開了一個協程,然後一直迴圈列印。

假設程式都還有很多其他的協程也在工作,發現這個協程工作太久了,系統會進行切換別的協程,現在這個協程會放入協程隊列中。

問題:要做到這點,協程需要怎麼保存這個執行狀態?

  1. 需要一個函數的調用棧,記錄執行了那些函數,(例子中只有一個,正常情況下會是很多函數相互調用) 函數執行完後,還需要回到上層函數,所以要保存函數棧信息。
  1. 需要記錄當前執行到了 那行代碼,不能把多執行,也不能少執行那句代碼,不然程式會不可控。
  1. 需要一個空間,存儲整個協程的數據,例如變數的值等。

協程的底層定義

在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() 意味著 為現在的線程,切換協程。

到此,和上面的圖都對應上了。但是目前還是單線程,多線程時候,是如何工作了,下篇再聊。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 主要探討了SpringMVC中的流程跳轉和不同形式的控制器之間的跳轉方式。首先回顧了JavaWeb中流程跳轉的核心代碼和頁面跳轉方式,並展示了在Web.xml中添加Servlet以及執行這些方式的示例。隨後,介紹了Spring MVC中的四種跳轉形式,包括控制器到JSP頁面的forward和redi... ...
  • 寫在前面 先吐槽兩句,搞個mysql安裝配置弄了4個小時,怎麼都是外網無法訪問,我靠,我特麽也是服了。 當然,後來我投降了,明天再說,學什麼不是學,娘的,換個方向,狀態依然在! Sijax是什麼? 代表 Simple Ajax ,它是一個 Python / jQuery 庫,使用 jQuery.aj ...
  • Sun公司提供了JavaMail用來實現郵件發送,但是配置煩瑣,Spring中提供了JavaMailSender用來簡化郵件配置,Spring Boot則提供了MailSenderAutoConfiguration對郵件的發送做了進一步簡化。 v準備工作 開通POP3/SMTP服務或者IMAP/SM ...
  • C語言分支結構詳解 1. if 語句 在本篇博客文章中,我們將深入探討C語言中的if語句及其相關用法。if語句是一種用於條件判斷的分支語句,它允許我們根據條件的真假來執行不同的代碼塊。 1.1 if 語句的基本語法和用法 if語句的基本語法如下所示: if (條件) { // 條件為真時執行的代碼塊 ...
  • CF786 我不會告訴你鏈接在圖片里 CF786A CF786A題意 給出一個大小為 \(n\) 的環,點順時針從 \(1\to n\) 編號,兩個人(設為 \(0,1\))輪流移動其中的一個棋子。 對於第 \(opt\) 人,他能夠將這個棋子順時針移動 \(x\in S_{opt}\)(\(S_{ ...
  • public class RandomNickName { public enum Gender{ MAN, WOMAN, UNKNOWN, ; } public static void main(String[] args) { String nickName = nickName(Gender. ...
  • 1 簡介 任務是需要資源(CPU 時間、記憶體、存儲、網路帶寬等)在指定時間內完成的一段計算工作。 通過智能地將資源分配給任務以滿足任務級和系統級目標的系統稱為任務調度程式。 任務調度程式: 及時決定和分配資源給任務的過程稱為任務調度。 當我們在 Facebook 發表評論時。我們不會讓評論發佈者等待 ...
  • 本文從源碼層面主要分析了線程池的創建、運行過程,通過上述的分析,可以看出當線程池中的線程數量超過核心線程數後,會先將任務放入等待隊列,隊列放滿後當最大線程數大於核心線程數時,才會創建新的線程執行。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...