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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...