golang gmp模型分析

思維導圖:

1. 發展過程

思維導圖:

在單機時代是沒有多線程、多進程、協程這些概念的。早期的操作系統都是順序執行

單進程的缺點有

  • 單一執行流程、計算機只能一個任務一個任務進行處理
  • 進程阻塞所帶來的CPU時間的浪費

處于對CPU資源的利用,發展出多線程/多進程操作系統,采用時間片輪訓算法

宏觀上來說,就算只有一個cpu,也能并發執行多個進程

這樣的好處是充分利用了CPU,但是也帶來了一些問題,例如時間片切換需要花費額外的開銷

  • 進程/線程的數量越多,切換成本就越大,也就越浪費

對于開發人員來說,盡管線程看起來很美好,但實際上多線程開發設計會變得更加復雜,要考慮很多同步競爭等問題,如競爭沖突

進程擁有太多的資源,進程的創建、切換、銷毀,都會占用很長的時間,CPU雖然利用起來了,但如果進程過多,CPU有很大的一部分都被用來進行進程調度

所以提高cpu的利用率成為我們需要解決的問題

既然問題出現在線程上下文切換中,那么首先我們需要好好想一想什么是線程的上下文切換

我們知道操作系統的一些核心接口是不能被進程隨意調度的,例如進行io流的讀寫操作,需要將最終的執行權交給操作系統(內核態)進行調度,所以就會有用戶態和內核態之前的切換

這個時候我們的線程模型是這樣的

一個線程需要在內核態用戶態之間進行切換,并且切換是受到操作系統控制的,可能這個現在需要等待多個時間片才能切換到內核態再調用操作系統底層的接口

那么我們是否可以用兩個線程分別處理這兩種狀態呢?兩個線程之間再做好綁定,當用戶線程將任務提交給內核線程后,就可以不用堵塞了,可以去執行其他的任務了

對于CPU來說(多核CPU),不需要關注線程切換的問題,只需要分配系統資源給內核線程進行調度即可

我們來給用戶線程換個名字——協程(co-runtine)

如果是一比一的關系的話,其實還是可能需要等待內核線程的執行

所以可以設計為N 比 1的形式,多個協程可以將任務一股腦的交給內核線程去完成,但是這樣又有問題,如果其中一個問題在提交任務的過程中,堵塞住了,就會影響其他線程的工作

這個就是python的event-loop遇到的問題,一個阻塞,其余全阻塞

所以一般為M 比 N的關系

M 比 N的關系中,大部分的精力都會放在協程調度器上,如果調度器效率高就能讓協程之間阻塞時間盡可能的少

在golang中對協程調度器協程內存進行了優化

  • 協程調度器:可以支持靈活調度
  • 內存輕量化:可以擁有大量的協程

在golang早期的協程調度器中,采用的是隊列的方式,M想要執行、放回G都必須訪問全局G隊列,并且M有多個,即多線程訪問同一資源需要加鎖進行保證互斥/同步,所以全局G隊列是有互斥鎖進行保護的

老調度器有幾個缺點:

  1. 創建、銷毀、調度G都需要每個M獲取鎖,這就形成了激烈的鎖競爭
  2. M轉移G會造成延遲和額外的系統負載。比如當G中包含創建新協程的時候,M創建了G',為了繼續執行G,需要把G'交給M'執行,也造成了很差的局部性,因為G'G是相關的,最好放在M上執行,而不是其他M'
  3. 系統調用(CPU在M之間的切換)導致頻繁的線程阻塞和取消阻塞操作增加了系統開銷

2. GMP模型設計思想

思維導圖:

2.1 GMP模型

GMP是goalng的線程模型,包含三個概念:內核線程(M),goroutine(G),G的上下文環境(P)

  • G:goroutine協程,基于協程建立的用戶態線程
  • M:machine,它直接關聯一個os內核線程,用于執行G
  • P:processor處理器,P里面一般會存當前goroutine運行的上下文環境(函數指針,堆棧地址及地址邊界),P會對自己管理的goroutine隊列做一些調度

在Go中,線程是運行goroutine的實體,調度器的功能是把可運行的goroutine分配到工作線程上

  1. 全局隊列(Global Queue):存放等待運行的G
  2. P的本地隊列:同全局隊列類似,存放的也是等待運行的G,存的數量有限,不超過256個。新建G'時,G'優先加入到P的本地隊列,如果隊列滿了,則會把本地隊列中一半的G移動到全局隊列
  3. P列表:所有的P都在程序啟動時創建,并保存在數組中,最多有GOMAXPROCS(可配置)個
  4. M:線程想運行任務就得獲取P,從P的本地隊列獲取GP隊列為空時,M也會嘗試從全局隊列拿一批G放到P的本地隊列,或從其他P的本地隊列一半放到自己P的本地隊列。M運行G,G執行之后,M會從P獲取下一個G,不斷重復下去

P和M的數量問題

  • P的數量:環境變量$GOMAXPROCS;在程序中通過runtime.GOMAXPROCS()來設置
  • M的數量:GO語言本身限定一萬?(但是操作系統達不到);通過runtime/debug包中的SetMaxThreads函數來設置;有一個M阻塞,會創建一個新的M;如果有M空閑,那么就會回收或者休眠

M與P的數量沒有絕對關系,一個M阻塞,P就會去創建或者切換另一個M,所以,即使P的默認數量是1,也有可能會創建很多個M出來

2.2 調度器的設計策略

golang調度器的設計策略思想主要有以下幾點:

  • 復用線程
  • 利用并行
  • 搶占
  • 全局G隊列
2.2.1 復用線程

golang在復用線程上主要體現在work stealing機制hand off機制(偷別人的去執行,和自己扔掉執行)

首先我們看work stealing,我們在學習java的時候學過fork/join,其中也是通過工作竊取方式來提升效率,充分利用線程進行并行計算,并減少了線程間的競爭

干完活的線程與其等著,不如去幫其他線程干活,于是它就去其他線程的隊列里竊取一個任務來執行。而在這時它們會訪問同一個隊列,所以為了減少竊取任務線程和被竊取任務線程之間的競爭,通常會使用雙端隊列被竊取任務線程永遠從雙端隊列的頭部拿任務執行,而竊取任務的線程永遠從雙端隊列的尾部拿任務執行

hand off機制

當本線程因為G進行系統調用阻塞時,線程釋放綁定的P,把P轉移給其他空閑的線程執行,此時M1如果長時間阻塞,可能會執行睡眠或銷毀

2.2.2 利用并行

我們可以使用GOMAXPROCS設置P的數量,這樣的話最多有GOMAXPROCS個線程分布在多個CPU上同時運行。GOMAXPROCS也限制了并發的程度,比如GOMAXPROCS = 核數/2,則最多利用了一半的CPU核進行并行


2.2.3 搶占策略

  • 1對1模型的調度器,需要等待一個co-routine主動釋放后才能輪到下一個進行使用
  • golang中,如果一個goroutine使用10ms還沒執行完,CPU資源就會被其他goroutine所搶占


2.2.4 全局G隊列

  • 全局G隊列其實是復用線程的補充,當工作竊取時,優先從全局隊列去取,取不到才從別的p本地隊列取(1.17版本)

  • 在新的調度器中依然有全局G隊列,但功能已經被弱化了,當M執行work stealing從其他P偷不到G時,它可以從全局G隊列獲取G

  • 從其他隊列的偷取過程是從隊列尾部偷取,而隊列的執行過程是順序執行,該隊列是雙端隊列可無鎖進行。

  • 每次偷取數量為隊列一半的量。

2.3?go func()經歷了那些過程

  1. 我們通過go func()來創建一個goroutine
  2. 有兩個存儲G的隊列,一個是局部調度器P的本地隊列、一個是全局G隊列。新創建的G會先保存在P的本地隊列中,如果P的本地隊列已經滿了就會保存在全局的隊列
  3. G只能運行在M中,一個M必須持有一個P,M與P是1:1的關系。M會從P的本地隊列彈出一個可執行狀態的G來執行,如果P的本地隊列為空,就會想其他的MP組合偷取一個可執行的G來執行
  4. 一個M調度G執行的過程是一個循環機制
  5. 當M執行某一個G時候如果發生了syscall或者其他阻塞操作,M會阻塞,如果當前有一些G在執行,runtime會把這個線程M從P中摘除(detach),然后再創建一個新的操作系統的線程(如果有空閑的線程可用就復用空閑線程)來服務于這個P
  6. 當M系統調用結束時候,這個G會嘗試獲取一個空閑的P執行,并放入到這個P的本地隊列。如果獲取不到P,那么這個線程M變成休眠狀態, 加入到空閑線程中,然后這個G會被放入全局隊列

2.4 調度器的生命周期

在了解調度器生命周期之前,我們需要了解兩個新的角色M0G0

M0(跟進程數量綁定,一比一):

  • 啟動程序后編號為0的主線程
  • 在全局變量runtime.m0中,不需要在heap上分配
  • 負責執行初始化操作和啟動第一個G
  • 啟動第一個G之后,M0就和其他的M一樣了

G0(每個M都會有一個G0):

  • 每次啟動一個M,都會第一個創建的gourtine,就是G0
  • G0僅用于負責調度G
  • G0不指向任何可執行的函數
  • 每個M都會有一個自己的G0
  • 在調度或系統調用時會使用M切換到G0,再通過G0進行調度

M0和G0都是放在全局空間的

具體流程為:

我們來分析一段代碼:

package mainimport "fmt"func main() {fmt.Println("Hello world")
}
  1. runtime創建最初的線程m0和goroutine g0,并把2者關聯。
  2. 調度器初始化:初始化m0、棧、垃圾回收,以及創建和初始化由GOMAXPROCSP構成的P列表
  3. 示例代碼中的main函數是main.mainruntime中也有1個main函數——runtime.main,代碼經過編譯后,runtime.main會調用main.main,程序啟動時會為runtime.main創建goroutine,稱它為main goroutine吧,然后把main goroutine加入到P的本地隊列。
  4. 啟動m0,m0已經綁定了P,會從P的本地隊列獲取G,獲取到main goroutine。
  5. G擁有棧,M根據G中的棧信息和調度信息設置運行環境
  6. M運行G
  7. G退出,再次回到M獲取可運行的G,這樣重復下去,直到main.main退出,runtime.main執行Defer和Panic處理,或調用runtime.exit退出程序。

調度器的生命周期幾乎占滿了一個Go程序的一生,runtime.main的goroutine執行之前都是為調度器做準備工作,runtime.main的goroutine運行,才是調度器的真正開始,直到runtime.main結束而結束

2.5?協程的主動讓渡與搶占

理解

我們已經知道,協程執行time.Sleep時,狀態會從_Grunning變為_Gwaiting ,并進入到對應timer中等待,而timer中持有一個回調函數,在指定時間到達后調用這個回調函數,把等在這里的協程恢復到_Grunnable狀態,并放回到runq中。

那誰負責在定時器時間到達時,觸發定時器注冊的回調函數呢?其實每個P都持有一個最小堆,存儲在P.timers中,用于管理自己的timer,堆頂timer就是接下來要觸發的那一個。

而每次調度時,都會調用checkTimers函數,檢查并執行已經到時間的那些timer,不過這還不夠穩妥,萬一所有M都在忙,不能及時觸發調度的話,可能會導致timer執行時間發生較大的偏差。

所以還會通過監控線程來增加一層保障,在介紹HelloGoroutine(GMP一)的執行過程時,我們提過監控線程是由main goroutine創建的,這個監控線程與GMP中的工作線程不同。并不需要依賴P,也不由GMP模型調度,它會重復執行一系列任務,只不過會視情況調整自己的休眠時間。其中一項任務便是保障timer正常執行,監控線程檢測到接下來有timer要執行時,不僅會按需調整休眠時間,還會在空不出M時創建新的工作線程,以保障timer可以順利執行。

當協程等待一個channel時,其狀態也會從_Grunnig變成_Gwaiting,并進入到對應的channel的讀隊列或寫隊列中等待。

如果協程需要等待IO事件,就也需要讓出,以epoll為例,若IO事件尚未就緒,需要注冊要等待的IO事件到監聽隊列中,而每個監聽對象都可以關聯一個event data。所以就在這里記錄是哪個協程在等待,等到事件就緒時再把它恢復到runq中即可 。

不過timer計時器有設置好的觸發時間 ,等待的channel可讀可寫或關閉了,也自會通知到相關協程,而獲取就緒的IO時間需要主動輪詢,所以為了降低IO延遲,需要時不時的那么輪詢一下,也就是執行netpoll。實際上監控線程,調度器,GC等工作過程中都會按需執行netpoll。

全局變量sched中會記錄上次netpoll執行的時間,監控線程檢測到距離上次輪詢已超過了10ms,就會再執行一次netpoll。

上面說的無一例外,都是協程會主動讓出的情況,那要是一個協程不會等待timer,channel或者IO事件,就不讓出了嗎?那必須不能啊,否則調度器豈不成了擺設?那怎么讓那些不用等待的協程”讓出“呢,這就是監控線程的另一個工作任務了,那就是本著公平調度的原則,對運行時間過長的G,實行”搶占“操作。

就是告訴那些運行時間超過特定閾值(10ms)的G,該讓一讓了,怎么知道運行時間過長了呢,P里面有一個schedtick字段,每當調度執行一個新的G,并且不繼承上個G的時間片時,就會把它自增1,而這個p.sysmontick中,schedwhen記錄的是上一次調度的時間,監控線程如果檢測到p.sysmontick.schedtick與p.schedtick不相等,說明這個P又發生了新的調度,就會同步這里的調度次數,并更新這個調度時間。

但是若2者相等,就說明自schedwhen這個時間點之后,這個P并未發生新的調度,或者即使發生了新的調度,也延用了之前G的時間片,所以可以通過當前時間與schedwhen的差值來判斷當前G是否運行時間過長了。

那如果真的運行時間過長了,要怎么通知它讓出呢?這就不得不提到棧增長了,除了對協程棧沒什么消耗的函數調用,Go語言編譯器都會在函數頭部插入棧相關代碼。實際上編譯器插入的棧增長代碼一共有三種。注意這里為什么是”<=“,棧是向下增長的,上面是高地址,下面是低地址

如果棧幀比較小,插入的代碼就是這樣的,這個SP表示當前協程棧使用到了什么位置,stackguard0是協程棧空間下界,所以當協程棧的消耗達到或超過這個位置時,就需要進行棧增長了。

如果棧幀大小處在_StackSmall和_StackBig之間,插入的代碼是這樣的,也就是說,當前協程棧使用到這里,若再使用framesize這么多,超出stackguard0的部分大于_StackSmall了,就要進行棧增長了

而對于棧幀大小超過_StackBig的函數,插入的代碼就有所有不同了,判斷是否要棧增長的方式,本質上同第二種情況相同,而我們要關注的,是這里的stackPreempt ,它是和協程調度相關的重要標識,當runtime希望某個協程讓出CPU時,就會把它的stackguard0賦值為stackPreempt。這是一個非常大的值,真正的棧指針不可能指向這個位置,所以可以安全的用作特殊標識。

正因為stackPreempt這個值足夠大,所以這兩段代碼種的判斷結果也都會為true,進而跳轉到morestack處。

而morestack‘這里,最終會調用runtime.newstack函數,它負責棧增長工作,不過它在進行棧增長之前,會先判斷stackguard0是否等于stackPreempt,等于的話就不進行棧增長了,而是執行一次協程調度。

所以在協程不主動讓出時,也可以設置stackPreempt標識,通知它讓出。

不過這種搶占方式的缺陷,就是過于依賴棧增長代碼,如果來個空的for循環,因為與棧增長無關,監控線程等也無法通過設置stackPreempt標識來實現搶占,所以最終導致程序卡死。

這一問題在1.14版本中得到了解決,因為它實現了異步搶占,具體實現在不同平臺種不盡相同。例如在Unix平臺中,會向協程關聯的M發送信號(sigPreempt),接下來目標線程會被信號中斷,轉去執行runtime.sighandler,在sighandler函數中檢測到函數信號為sigPreempt后,就會調用runtime.doSigPreempt函數,它會向當前被打斷的協程上下文中,注入一個異步搶占函數調用,處理完信號后sighandler返回,被中斷的協程得以恢復,立刻執行被注入的異步搶占函數, 該函數最終會調用runtime中的調度邏輯,這不就讓出了嘛。所以在1.14版本中,這段代碼執行之前就不會卡死了。

而監控線程的搶占方式又多了一種,異步搶占,其實為了充分利用CPU,監控線程還會搶占處在系統調用中的P,因為一個協程要執行系統調用,就要切換到g0棧,在系統調用沒執行完之前,這個M和這個G算是抱團了,不能被分開,也就用不到P,所以在陷入系統調用之前,當前M會讓出P,解除m.p與當前p的強關聯,只在m.oldp中記錄這個p,P的數目畢竟有限,如果有其他協程在等待執行,那么放任P如此閑置就著實浪費了,還是把它關聯到其他M繼續工作比較劃算,不過如果當前M從系統調用中恢復,會先檢測之前的P是否被占用,沒有的話就繼續使用,否則就再去申請一個,沒申請到的話,就把當前G放到全局runq中去,然后當前線程m就睡眠了。

說了這么多,不是讓出就是搶占。

那讓出了,搶占了之后,M也不能閑著,得找到下一個待執行的G來運行,這就是schedule()的職責了。schedul這里要給這個M找到一個待執行的G,首先要確定當前M是否和當前G綁定了,如果綁定了,那當前M就不能執行其他G,所以需要阻塞當前M,等到當前G再次得到調度執行時,自會把當前M喚醒。如果沒有綁定,就先看看GC是不是在等待執行,全局變量sched這里,有一個gcwaiting標識,如果GC在等待執行,就去執行GC,回來再繼續執行調度程序。接下來還會檢查一下有沒有要執行的timer。調度程序還有一定幾率會去全局runq中獲取一部分G到本地runq中。

而獲取下一個待執行的G時,會先去本地runq中查找,沒有的話,就調用findrunnable(),這個函數直到獲取到待運行的G才會返回。在findrunnable()函數這里,也會判斷是否要執行GC,然后先嘗試從本地runq中獲取,沒有的話就從全局runq獲取一部分,如果還沒有,就先嘗試執行netpoll,恢復那些IO事件已經就緒了的G,它們會被放回到全局runq中,然后才會嘗試從其他P那里steal一些G 。

當調度程序終于獲得一個待執行的G以后,還要看看人家有沒有綁定的M,如果有的話還得乖乖的把G還給對應的M。而當前M就不得不再次進行調度了。如果沒有綁定的M,就調用excute函數在當前M上執行這個G。excute函數這里會簡歷當前M和這個G的關聯關系,并把G的狀態從_Grunnable修改為_Grunning,如果不繼承上一個執行中協程的時間片,就把P這里的調度計數加一,最后會調用gogo函數,從g.sched這里恢復協程棧指針,指令指針等,接著繼續協程的執行。

之前介紹過,協程創建時,會偽裝一個執行現場存到g.sched中,所以即使這個G初次執行,也是有一個完美的執行現場的。

現在我們已經知道,協程在某些情況下會主動讓出,但有時也需要設置stackPreemt標識,或異步搶占的方式來通知它讓出。也了解了調度程序如何獲取待執行的G并把它運行起來。期間還穿插介紹了監控線程的主要工作任務”保障計時器正常工作,執行網絡輪詢,搶占長時間運行的,或處在系統調用的P“,這些都是為了保障程序健康高效的執行,其實監控線程還有一項任務,就是強制執行GC,待到內存管理部分再展開~

2.6 可視化的CMP編程

2.6.1 trace方式

在這里我們需要使用trace編程,三步走:

  1. 創建trace文件:f, err := os.Create("trace.out")
  2. 啟動trace:err = trace.Start(f)
  3. 停止trace:trace.Stop()

然后再通過go tool trace工具打開trace文件

go tool trace trace.out
package mainimport ("fmt""os""runtime/trace"
)// trace的編碼過程
// 1. 創建文件
// 2. 啟動
// 3. 停止
func main() {// 1.創建一個trace文件f, err := os.Create("trace.out")if err != nil {panic(err)}defer func(f *os.File) {err := f.Close()if err != nil {panic(err)}}(f)// 2. 啟動traceerr = trace.Start(f)if err != nil {panic(err)}// 正常要調試的業務fmt.Println("hello GMP")// 3. 停止tracetrace.Stop()
}

打開后我們進入網頁點擊view trace,然后就能看到分析信息

G的信息

M的信息

P的信息

2.6.2 debug方式

使用debug方式可以不需要trace文件

先搞一段代碼

package mainimport ("fmt""time"
)func main() {for i := 0; i < 5; i++ {time.Sleep(time.Second)fmt.Println("hello GMP")}
}

debug執行一下

$ GODEBUG=schedtrace=1000 ./debug.exe
SCHED 0ms: gomaxprocs=8 idleprocs=7 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
SCHED 1008ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED 2009ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED 3010ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
hello GMP
SCHED 4017ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
  • SCHED:調試信息輸出標志字符串,代表本行是goroutine調度器的輸出;
  • 0ms:即從程序啟動到輸出這行日志的時間;
  • gomaxprocs: P的數量,本例有2個P, 因為默認的P的屬性是和cpu核心數量默認一致,當然也可以通過GOMAXPROCS來設置;
  • idleprocs: 處于idle狀態的P的數量;通過gomaxprocs和idleprocs的差值,我們就可知道執行go代碼的P的數量;
  • threads: os threads/M的數量,包含scheduler使用的m數量,加上runtime自用的類似sysmon這樣的thread的數量;
  • spinningthreads: 處于自旋狀態的os thread數量;
  • idlethread: 處于idle狀態的os thread的數量;
  • runqueue=0: Scheduler全局隊列中G的數量;
  • [0 0]: 分別為2個P的local queue中的G的數量。

參考文章

GolangGMP模型 GMP(三):協程讓出,搶占,監控與調度 - cheems~ - 博客園

https://github.com/fengyuan-liang/notes/blob/main/GoLang/golang%E5%A4%A7%E6%9D%80%E5%99%A8GMP%E6%A8%A1%E5%9E%8B.md

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/75234.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/75234.shtml
英文地址,請注明出處:http://en.pswp.cn/web/75234.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Redis基礎指令(Windows)

1.cmd命令行啟動redis 直接cmd打開整個文件 1.1.啟動server 輸入指令&#xff1a; redis-server.exe redis.windows.conf 會進入serve端 1.2.啟動客戶端 &#xff01;&#xff01;重新打開一個cmd&#xff0c;方法和上面一樣&#xff01;&#xff01; 之后輸入 redis-…

vue:前端預覽 / chrome瀏覽器設置 / <iframe> 方法預覽 doc、pdf / vue-pdf 預覽pdf

一、本文目標 <iframe> 方法預覽 pdf 、word vue-pdf 預覽pdf 二、<iframe> 方法 2.1、iframe 方法預覽需要 瀏覽器 設置為&#xff1a; chrome&#xff1a;設置-隱私設置和安全性-網站設置-更多內容設置-PDF文檔 瀏覽器訪問&#xff1a; chrome://settings/co…

【C++游戲引擎開發】第11篇:GLFW、GLAD環境搭建與第一個三角形渲染

一、GLFW、GLAD安裝 1.1 vcpkg安裝相關庫 跨平臺C++包管理利器vcpkg完全指南 # 安裝GLFW vcpkg install glfw3# 安裝GLAD vcpkg install glad1.2 初始測試代碼 #include <glad/glad.h> #include <GLFW/glfw3.h> int main() {glfwInit();GLFWwindow* window = g…

西門子S7-1500與S7-200SMART通訊全攻略:從基礎配置到遠程IO集成

以下是一篇關于西門子S7-1500與S7-200SMART通訊的詳細教程&#xff0c;包含遠程IO模塊的配置方法&#xff0c;適用于工業自動化場景的博客發布&#xff1a; 西門子S7-1500與S7-200SMART通訊全攻略&#xff1a;從基礎配置到遠程IO集成 一、硬件與軟件準備 硬件設備 主站&#x…

前端性能優化的全方位方案【待進一步結合項目】

以下是前端性能優化的全方位方案,結合代碼配置和最佳實踐,涵蓋從代碼編寫到部署的全流程優化: 一、代碼層面優化 1. HTML結構優化 <!-- 語義化標簽減少嵌套 --> <header><nav>...</nav> </header> <main><article>...</arti…

前端快速入門——JavaScript變量、控制語句

1.JavaScript 定義 JavaScript 簡稱 JS. JavaScript 是一種輕量級、解釋型、面向對象的腳本語言。它主要被設計用于在網頁上實現動態效果&#xff0c;增加用戶與網頁的交互性。 作為一種客戶端腳本語言&#xff0c;JavaScript 可以直接嵌入 HTML&#xff0c;并在瀏覽器中執行。…

GitHub 趨勢日報 (2025年04月01日)

GitHub 趨勢日報 (2025年04月01日) 本日報由 TrendForge 系統生成 https://trendforge.devlive.org/ &#x1f4c8; 今日整體趨勢 Top 10 排名項目名稱項目描述今日獲星語言1punkpeye/awesome-mcp-serversA collection of MCP servers.? 3280未指定2th-ch/youtube-musicYouTu…

windows手動添加鼠標右鍵彈窗快捷方式

此處以添加Git Bash Here為例 一.操作步驟 按 Win R 鍵打開 運行 對話框&#xff0c;輸入 regedit&#xff0c;并按下回車&#xff0c;打開注冊表編輯器。 導航到 HKEY_CLASSES_ROOT\Directory\Background\shell。 右鍵單擊 shell&#xff0c;選擇 新建 → 項&#xff0c;并…

2025.04.09【Sankey】| 生信數據流可視化精講

文章目錄 引言Sankey圖簡介R語言中的Sankey圖實現安裝和加載networkD3包創建Sankey圖的數據結構創建Sankey圖繪制Sankey圖 結論 引言 在生物信息學領域&#xff0c;數據可視化是理解和分析復雜數據集的關鍵工具之一。今天&#xff0c;我們將深入探討一種特別適用于展示數據流動…

GD32H759IMT6 Cortex-M7 OpenHarmony輕量系統移植——4.1版本升級到5.0.3

筆者在去年利用國慶時間&#xff0c;將Cortex-M7 的國產廠商兆易創新GD32H459移植OpenHarmony輕量系統&#xff0c;但是適配不太完善——只能選擇liteos-m接管中斷。這樣導致使用中斷非常麻煩。于是筆者最近將接管中斷模式修改為不接管&#xff0c;這樣可以方便的使用gd32提供的…

【算法競賽】樹上最長公共路徑前綴(藍橋杯2024真題·團建·超詳細解析)

目錄 一、題目 二、思路 1. 問題轉化&#xff1a;同步DFS走樹 2. 優化&#xff1a;同步DFS匹配 3. 狀態設計&#xff1a;dfs參數含義 4. 匹配過程&#xff1a;用 map 建立權值索引 5. 終止條件&#xff1a;無法匹配則更新答案 6. 總結 三、完整代碼 四、知識點總…

開源免費虛擬化平臺PVE軟件定義網絡

一、PVE SDN&#xff08;Software Defined Networking&#xff09;原理與使用邏輯 SDN&#xff08;軟件定義網絡&#xff09; 是一種將網絡控制邏輯從傳統交換機、路由器中分離出來的技術&#xff0c;使得網絡可以通過軟件集中管理和自動化配置。 Proxmox VE&#xff08;PVE&…

mysql 8.0.41下載安裝教程(附安裝包)mysql 8.0.41圖文詳細安裝教程

文章目錄 前言一、mysql 8.0.41 簡介二、安裝前準備三、MySQL 8.0 安裝流程解析1.解壓安裝包2.啟動安裝程序3.選擇安裝類型4.選擇安裝組件5.開始安裝6.配置設置&#xff08;部分步驟&#xff09;7.設置數據庫密碼8.完成安裝配置9.配置環境變量&#xff1a;10.驗證安裝&#xff…

JAVA基礎八股復習

1.局部變量一般存放在棧中&#xff0c;成員變量一般存放在堆中 2.什么是多態&#xff1f;談談對多態的理解&#xff1f; 在面向對象語言中&#xff0c;接口的多種不同的實現方式即為多態。用白話來說&#xff0c;就是多個對象調用同一個方法&#xff0c;得到不同的結果。 多態中…

10:00開始面試,10:08就出來了,問的問題有點變態。。。

從小廠出來&#xff0c;沒想到在另一家公司又寄了。 到這家公司開始上班&#xff0c;加班是每天必不可少的&#xff0c;看在錢給的比較多的份上&#xff0c;就不太計較了。沒想到8月一紙通知&#xff0c;所有人不準加班&#xff0c;加班費不僅沒有了&#xff0c;薪資還要降40%…

k8s核心資源對象一(入門到精通)

本文將深入探討Kubernetes中的核心資源對象&#xff0c;包括Pod、Deployment、Service、Ingress、ConfigMap和Secret&#xff0c;詳細解析其概念、功能以及實際應用場景&#xff0c;幫助讀者全面掌握這些關鍵組件的使用方法。 一、pod 1 pod概念 k8s最小調度單元&#xff0c;…

《Sqoop 快速上手:安裝 + 測試實戰》

推薦原文 見&#xff1a;http://docs.xupengboo.top/bigdata/di/sqoop.html Sqoop&#xff08;SQL-to-Hadoop&#xff09; 是 Apache 開源的工具&#xff0c;專門用于在 Hadoop 生態系統&#xff08;如 HDFS、Hive、HBase&#xff09; 和 關系型數據庫&#xff08;如 MySQL、O…

數據結構刷題之貪心算法

貪心算法&#xff08;Greedy Algorithm&#xff09; 是一種在每個步驟中都選擇當前最優解的算法設計策略。它通常用于解決優化問題&#xff0c;例如最小化成本或最大化收益。貪心算法的核心思想是&#xff1a;在每一步選擇中&#xff0c;都做出局部最優的選擇&#xff0c;希望…

重新定義PPT創作!ChatPPT發布全球首個AI PPT專用MCP Server

在這個AI技術日新月異的時代&#xff0c;ChatPPT團隊推出革命性的MCP Server&#xff08;Multimodal Collaboration Platform&#xff09;&#xff0c;這是全球首個專注于AI PPT生成領域的智能協作平臺。該平臺的誕生&#xff0c;標志著PPT創作正式邁入"智能協作"新紀…

未來蓉城:科技與生態共舞的詩意棲居-成都

故事背景 故事發生在中國四川成都的2075年&#xff0c;展現科技與自然深度交融的未來城市圖景。通過六個充滿想象力的生態裝置場景&#xff0c;描繪市民在智慧城市中詩意棲居的生活狀態&#xff0c;展現環境保護與人文傳承的和諧共生。 故事內容 在電子竹林輕軌站&#xff0c;通…