slice 擴容機制
GO1.17版本及之前
當新切片需要的容量cap大於兩倍擴容的容量,則直接按照新切片需要的容量擴容;
當原 slice 容量 < 1024 的時候,新 slice 容量變成原來的 2 倍;
當原 slice 容量 > 1024,進入一個迴圈,每次容量變成原來的1.25倍,直到大於期望容量。
GO1.18之後
當新切片需要的容量cap大於兩倍擴容的容量,則直接按照新切片需要的容量擴容;
當原 slice 容量 < threshold 的時候,新 slice 容量變成原來的 2 倍;
當原 slice 容量 > threshold,進入一個迴圈,每次容量增加(舊容量+3*threshold)/4。
slice為什麼不是執行緒安全的
slice底層結構並沒有使用加鎖的方式,不支援併發讀寫
map 底層原理
map 是一個指標 佔用8個位元組(64位計算機),指向hmap結構體,hmap包含多個bmap陣列(桶)
type hmap struct {
count int //元素個數,呼叫len(map)時直接返回
flags uint8 //標誌map當前狀態,正在刪除元素、新增元素.....
B uint8 //單元(buckets)的對數 B=5表示能容納32個元素 B隨著map容量增大而變大
noverflow uint16 //單元(buckets)溢位數量,如果一個單元能存8個key,此時儲存了9個,溢位了,就需要再增加一個單元
hash0 uint32 //雜湊種子
buckets unsafe.Pointer //指向單元(buckets)陣列,大小為2^B,可以為nil
oldbuckets unsafe.Pointer //擴容的時候,buckets長度會是oldbuckets的兩倍
nevacute uintptr //指示擴容進度,小於此buckets遷移完成
extra *mapextra //與gc相關 可選欄位
}
type bmap struct {
tophash [bucketCnt]uint8
}
//實際上編譯期間會生成一個新的資料結構
type bmap struct {
topbits [8]uint8 //key hash值前8位 用於快速定位keys的位置
keys [8]keytype //鍵
values [8]valuetype //值
pad uintptr
overflow uintptr //指向溢位桶 無符號整形 優化GC
}
map 擴容機制
擴容時機:向 map 插入新 key 的時候,會進行條件檢測,符合下面這 2 個條件,就會觸發擴容
擴容條件:
1.超過負載 map元素個數 > 6.5(負載因子) * 桶個數
2.溢位桶太多
當桶總數<2^15時,如果溢位桶總數>=桶總數,則認為溢位桶過多
當桶總數>2^15時,如果溢位桶總數>=2^15,則認為溢位桶過多
擴容機制:
雙倍擴容:針對條件1,新建一個buckets陣列,新的buckets大小是原來的2倍,然後舊buckets資料搬遷到新的buckets。
等量擴容:針對條件2,並不擴大容量,buckets數量維持不變,重新做一遍類似雙倍擴容的搬遷動作,把鬆散的鍵值對重新排列一次,使得同一個 bucket 中的 key 排列地更緊密,節省空間,提高 bucket 利用率,進而保證更快的存取。
漸進式擴容:
插入修改刪除key的時候,都會嘗試進行搬遷桶的工作,每次都會檢查oldbucket是否nil,如果不是nil則每次搬遷2個桶,螞蟻搬家一樣漸進式擴容
map 遍歷為什麼無序
map每次遍歷,都會從一個隨機值序號的桶,再從其中隨機的cell開始遍歷,並且擴容後,原來桶中的key會落到其他桶中,本身就會造成失序
如果想順序遍歷map,先把key放到切片排序,再按照key的順序遍歷map
var sl []int
for k := range m {
sl = append(sl, k)
}
sort.Ints(sl)
for _,k:= range sl {
fmt.Print(m[k])
}
map為什麼不是執行緒安全的
map設計就不是用來多個協程高併發訪問的
多個協程同時對map進行併發讀寫,程式會panic
如果想執行緒安全,可以使用sync.RWLock 鎖
sync.map
這個包裡面的map實現了鎖,是執行緒安全的
Map 如何查詢
1.防寫機制
先插hmap的標誌位flags,如果flags寫標誌位此時是1,說明其他協程正在寫操作,直接panic
2.計算hash值
key經過雜湊函式計算後,得到64bit(64位CPU)
10010111 | 101011101010110101010101101010101010 | 10010
3.找到hash對應的桶
上面64位後5(hmap的B值)位定位所存放的桶
如果當前正在擴容中,並且定位到舊桶資料還未完成遷移,則使用舊的桶
4.遍歷桶查詢
上面64位前8位用來在tophash陣列查詢快速判斷key是否在當前的桶中,如果不在需要去溢位桶查詢
5.返回key對應的指標
GO採用鏈地址法解決衝突,具體就是插入key到map中時,當key定位的桶填滿8個元素後,將會建立一個溢位桶,並且將溢位桶插入當前桶的所在連結串列尾部
負載因子 = 雜湊表儲存的元素個數 / 桶個數
Go 官方發現:裝載因子越大,填入的元素越多,空間利用率就越高,但發生雜湊衝突的機率就變大。
裝載因子越小,填入的元素越少,衝突發生的機率減小,但空間浪費也會變得更多,而且還會提高擴容操作的次數
Go 官方取了一個相對適中的值,把 Go 中的 map 的負載因子硬編碼為 6.5,這就是 6.5 的選擇緣由。
這意味著在 Go 語言中,當 map儲存的元素個數大於或等於 6.5 * 桶個數 時,就會觸發擴容行為。
type Map struct {
mu Mutex
read atomic.Value
dirty map[interface()]*entry
misses int
}
對比原始map:
和原始map+RWLock的實現併發的方式相比,減少了加鎖對效能的影響。它做了一些優化:可以無鎖訪問read map,而且會優先操作read map,倘若只操作read map就可以滿足要求,那就不用去操作write map(dirty),所以在某些特定場景中它發生鎖競爭的頻率會遠遠小於map+RWLock的實現方式
優點:
適合讀多寫少的場景
缺點:
寫多的場景,會導致 read map 快取失效,需要加鎖,衝突變多,效能急劇下降
通過var宣告或者make函式建立的channel變數是一個儲存在函式棧幀上的指標,佔用8個位元組,指向堆上的hchan結構體
type hchan struct {
closed uint32 // channel是否關閉的標誌
elemtype *_type // channel中的元素型別
// channel分為無緩衝和有緩衝兩種。
// 對於有緩衝的channel儲存資料,使用了 ring buffer(環形緩衝區) 來快取寫入的資料,本質是迴圈陣列
// 為啥是迴圈陣列?普通陣列不行嗎,普通陣列容量固定更適合指定的空間,彈出元素時,普通陣列需要全部都前移
// 當下標超過陣列容量後會回到第一個位置,所以需要有兩個欄位記錄當前讀和寫的下標位置
buf unsafe.Pointer // 指向底層迴圈陣列的指標(環形緩衝區)
qcount uint // 迴圈陣列中的元素數量
dataqsiz uint // 迴圈陣列的長度
elemsize uint16 // 元素的大小
sendx uint // 下一次寫下標的位置
recvx uint // 下一次讀下標的位置
// 嘗試讀取channel或向channel寫入資料而被阻塞的goroutine
recvq waitq // 讀等待佇列
sendq waitq // 寫等待佇列
lock mutex //互斥鎖,保證讀寫channel時不存在併發競爭問題
}
等待佇列:
雙向連結串列,包含一個頭結點和一個尾結點
每個節點是一個sudog結構體變數,記錄哪個協程在等待,等待的是哪個channel,等待傳送/接收的資料在哪裡
type waitq struct {
first *sudog
last *sudog
}
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer
c *hchan
...
}
建立時:
建立時會做一些檢查:
- 元素大小不能超過 64K
- 元素的對齊大小不能超過 maxAlign 也就是 8 位元組
- 計算出來的記憶體是否超過限制
建立時的策略:
- 如果是無緩衝的 channel,會直接給 hchan 分配記憶體
- 如果是有緩衝的 channel,並且元素不包含指標,那麼會為 hchan 和底層陣列分配一段連續的地址
- 如果是有緩衝的 channel,並且元素包含指標,那麼會為 hchan 和底層陣列分別分配地址
傳送時:
- 如果 channel 的讀等待佇列存在接收者goroutine
- 將資料**直接傳送**給第一個等待的 goroutine, **喚醒接收的 goroutine**
- 如果 channel 的讀等待佇列不存在接收者goroutine
- 如果迴圈陣列buf未滿,那麼將會把資料傳送到迴圈陣列buf的隊尾
- 如果迴圈陣列buf已滿,這個時候就會走阻塞傳送的流程,將當前 goroutine 加入寫等待佇列,並**掛起等待喚醒**
接收時:
- 如果 channel 的寫等待佇列存在傳送者goroutine
- 如果是無緩衝 channel,**直接**從第一個傳送者goroutine那裡把資料拷貝給接收變數,**喚醒傳送的 goroutine**
- 如果是有緩衝 channel(已滿),將迴圈陣列buf的隊首元素拷貝給接收變數,將第一個傳送者goroutine的資料拷貝到 buf迴圈陣列隊尾,**喚醒傳送的 goroutine**
- 如果 channel 的寫等待佇列不存在傳送者goroutine
- 如果迴圈陣列buf非空,將迴圈陣列buf的隊首元素拷貝給接收變數
- 如果迴圈陣列buf為空,這個時候就會走阻塞接收的流程,將當前 goroutine 加入讀等待佇列,並**掛起等待喚醒**
channel有2種型別:無緩衝、有緩衝
channel有3種模式:寫操作模式(單向通道)、讀操作模式(單向通道)、讀寫操作模式(雙向通道)
寫操作模式 make(chan<- int)
讀操作模式 make(<-chan int)
讀寫操作模式 make(chan int)
channel有3種狀態:未初始化、正常、關閉
操作\狀態 | 未初始化 | 關閉 | 正常 |
---|---|---|---|
關閉 | panic | panic | 正常 |
傳送 | 永遠阻塞導致死鎖 | panic | 阻塞或者成功傳送 |
接收 | 永遠阻塞導致死鎖 | 緩衝區為空則為零值, 否則可以繼續讀 | 阻塞或者成功接收 |
注意點:
一個 channel不能多次關閉,會導致painc
如果多個 goroutine 都監聽同一個 channel,那麼 channel 上的資料都可能隨機被某一個 goroutine 取走進行消費
如果多個 goroutine 監聽同一個 channel,如果這個 channel 被關閉,則所有 goroutine 都能收到退出訊號
不同協程通過channel進行通訊,本身的使用場景就是多執行緒,為了保證資料的一致性,必須實現執行緒安全
channel的底層實現中,hchan結構體中採用Mutex鎖來保證資料讀寫安全。在對迴圈陣列buf中的資料進行入隊和出隊操作時,必須先獲取互斥鎖,才能操作channel資料
func deadlock1() { //無緩衝channel只寫不讀
ch := make(chan int)
ch <- 3 // 這裡會發生一直阻塞的情況,執行不到下面一句
}
func deadlock2() { //無緩衝channel讀在寫後面
ch := make(chan int)
ch <- 3 // 這裡會發生一直阻塞的情況,執行不到下面一句
num := <-ch
fmt.Println("num=", num)
}
func deadlock3() { //無緩衝channel讀在寫後面
ch := make(chan int)
ch <- 100 // 這裡會發生一直阻塞的情況,執行不到下面一句
go func() {
num := <-ch
fmt.Println("num=", num)
}()
time.Sleep(time.Second)
}
func deadlock3() { //有緩衝channel寫入超過緩衝區數量
ch := make(chan int, 3)
ch <- 3
ch <- 4
ch <- 5
ch <- 6 // 這裡會發生一直阻塞的情況
}
func deadlock4() { //空讀
ch := make(chan int)
// ch := make(chan int, 1)
fmt.Println(<-ch) // 這裡會發生一直阻塞的情況
}
func deadlock5() { //互相等對方造成死鎖
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for {
select {
case num := <-ch1:
fmt.Println("num=", num)
ch2 <- 100
}
}
}()
for {
select {
case num := <-ch2:
fmt.Println("num=", num)
ch1 <- 300
}
}
}
Go sync包提供了兩種鎖型別:互斥鎖sync.Mutex 和 讀寫互斥鎖sync.RWMutex,都屬於悲觀鎖。
鎖的實現一般會依賴於原子操作、訊號量,通過atomic 包中的一些原子操作來實現鎖的鎖定,通過訊號量來實現執行緒的阻塞與喚醒
在正常模式下,鎖的等待者會按照先進先出的順序獲取鎖。但是剛被喚起的 Goroutine 與新建立的 Goroutine 競爭時,大概率會獲取不到鎖,在這種情況下,這個被喚醒的 Goroutine 會加入到等待佇列的前面。 如果一個等待的 Goroutine 超過1ms 沒有獲取鎖,那麼它將會把鎖轉變為飢餓模式。
Go在1.9中引入優化,目的保證互斥鎖的公平性。在飢餓模式中,互斥鎖會直接交給等待佇列最前面的 Goroutine。新的 Goroutine 在該狀態下不能獲取鎖、也不會進入自旋狀態,它們只會在佇列的末尾等待。如果一個 Goroutine 獲得了互斥鎖並且它在佇列的末尾或者它等待的時間少於 1ms,那麼當前的互斥鎖就會切換回正常模式。
執行緒沒有獲取到鎖時常見有2種處理方式:
- 一種是沒有獲取到鎖的執行緒就一直迴圈等待判斷該資源是否已經釋放鎖,這種鎖也叫做自旋鎖,它不用將執行緒阻塞起來, 適用於併發低且程式執行時間短的場景,缺點是cpu佔用較高
- 另外一種處理方式就是把自己阻塞起來,會釋放CPU給其他執行緒,核心會將執行緒置為「睡眠」狀態,等到鎖被釋放後,核心會在合適的時機喚醒該執行緒,適用於高併發場景,缺點是有執行緒上下文切換的開銷
Go語言中的Mutex實現了自旋與阻塞兩種場景,當滿足不了自旋條件時,就會進入阻塞
**允許自旋的條件:**
1. 鎖已被佔用,並且鎖不處於飢餓模式。
2. 積累的自旋次數小於最大自旋次數(active_spin=4)。
3. cpu 核數大於 1。
4. 有空閒的 P。
5. 當前 goroutine 所掛載的 P 下,本地待執行佇列為空。
讀寫鎖的底層是基於互斥鎖實現的。
寫鎖需要阻塞寫鎖:一個協程擁有寫鎖時,其他協程寫鎖定需要阻塞;
寫鎖需要阻塞讀鎖:一個協程擁有寫鎖時,其他協程讀鎖定需要阻塞;
讀鎖需要阻塞寫鎖:一個協程擁有讀鎖時,其他協程寫鎖定需要阻塞;
讀鎖不能阻塞讀鎖:一個協程擁有讀鎖時,其他協程也可以擁有讀鎖。
Go atomic包是最輕量級的鎖(也稱無鎖結構),可以在不形成臨界區和建立互斥量的情況下完成併發安全的值替換操作,不過這個包只支援int32/int64/uint32/uint64/uintptr這幾種資料型別的一些基礎操作(增減、交換、載入、儲存等)
當我們想要對**某個變數**併發安全的修改,除了使用官方提供的 `mutex`,還可以使用 sync/atomic 包的原子操作,它能夠保證對變數的讀取或修改期間不被其他的協程所影響。
atomic 包提供的原子操作能夠確保任一時刻只有一個goroutine對變數進行操作,善用 atomic 能夠避免程式中出現大量的鎖操作。
**常見操作:**
- 增減Add AddInt32 AddInt64 AddUint32 AddUint64 AddUintptr
- 載入Load LoadInt32 LoadInt64 LoadPointer LoadUint32 LoadUint64 LoadUintptr
- 比較並交換CompareAndSwap CompareAndSwapInt32...
- 交換Swap SwapInt32...
- 儲存Store StoreInt32...
原子操作由底層硬體支援,而鎖是基於原子操作+訊號量完成的。若實現相同的功能,前者通常會更有效率
原子操作是單個指令的互斥操作;互斥鎖/讀寫鎖是一種資料結構,可以完成臨界區(多個指令)的互斥操作,擴大原子操作的範圍
原子操作是無鎖操作,屬於樂觀鎖;說起鎖的時候,一般屬於悲觀鎖
原子操作存在於各個指令/語言層級,比如“機器指令層級的原子操作”,“彙編指令層級的原子操作”,“Go語言層級的原子操作”等。
鎖也存在於各個指令/語言層級中,比如“機器指令層級的鎖”,“彙編指令層級的鎖”,“Go語言層級的鎖”等
g本質是一個資料結構,真正讓 goroutine 執行起來的是排程器
type g struct {
goid int64 // 唯一的goroutine的ID
sched gobuf // goroutine切換時,用於儲存g的上下文
stack stack // 棧
gopc // pc of go statement that created this goroutine
startpc uintptr // pc of goroutine function ...
}
type gobuf struct { //執行時暫存器
sp uintptr // 棧指標位置
pc uintptr // 執行到的程式位置
g guintptr // 指向 goroutine
ret uintptr // 儲存系統呼叫的返回值 ...
}
type stack struct { //執行時棧
lo uintptr // 棧的下界記憶體地址
hi uintptr // 棧的上界記憶體地址
}
記憶體佔用:
建立一個 goroutine 的棧記憶體消耗為 2 KB,實際執行過程中,如果棧空間不夠用,會自動進行擴容。建立一個 thread 則需要消耗 1 MB 棧記憶體。
建立和銷毀:
Thread 建立和銷毀需要陷入核心,系統呼叫。而 goroutine 因為是由 Go runtime 負責管理的,建立和銷燬的消耗非常小,是使用者級。
切換:
當 threads 切換時,需要儲存各種暫存器,而 goroutines 切換隻需儲存三個暫存器:Program Counter, Stack Pointer and BP。一般而言,執行緒切換會消耗 1000-1500 ns,Goroutine 的切換約為 200 ns,因此,goroutines 切換成本比 threads 要小得多。
洩露原因
Goroutine 內進行channel/mutex 等讀寫操作被一直阻塞。
Goroutine 內的業務邏輯進入死迴圈,資源一直無法釋放。
Goroutine 內的業務邏輯進入長時間等待,有不斷新增的 Goroutine 進入等待
洩露場景
channel 如果忘記初始化,那麼無論你是讀,還是寫操作,都會造成阻塞。
channel 傳送數量 超過 channel接收數量,就會造成阻塞
channel 接收數量 超過 channel傳送數量,也會造成阻塞
http request body未關閉,goroutine不會退出
互斥鎖忘記解鎖
sync.WaitGroup使用不當
如何排查
單個函式:呼叫 `runtime.NumGoroutine` 方法來列印 執行程式碼前後Goroutine 的執行數量,進行前後比較,就能知道有沒有洩露了。
生產/測試環境:使用`PProf`實時監測Goroutine的數量
package main
import (
"net/http"
_ "net/http/pprof"
)
func main() {
for i := 0; i < 100; i++ {
go func() {
select {}
}()
}
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
select {}
}
執行程式之後,命令執行以下命令,會自動開啟瀏覽器顯示一系列目前還看不懂的圖,提示Could not execute dot; may need to install graphviz.則需要安裝graphviz,需要python環境
go tool pprof -http=:1248 http://127.0.0.1:6060/debug/pprof/goroutine
在開發過程中,如果不對goroutine加以控制而進行濫用的話,可能會導致服務整體崩潰。比如耗盡系統資源導致程式崩潰,或者CPU使用率過高導致系統忙不過來。
解決方案:
有緩衝channel:利用緩衝滿時傳送阻塞的特性
無緩衝channel:任務傳送和執行分離,指定消費者併發協程數
M個執行緒對應N個核心執行緒
優點:
- 能夠利用多核
- 上下文切換成本低
- 如果程式中的一個執行緒被阻塞,不會阻塞其他執行緒,是能夠切換同一程式內的其他執行緒繼續執行
G:Goroutine
M: 執行緒
P: Processor 本地佇列
GM模型:
2012年前的排程器模型,使用了4年果斷被拋棄,缺點如下:
1. 建立、銷燬、排程G都需要每個M獲取鎖,這就形成了激烈的鎖競爭。
2. M轉移G會造成延遲和額外的系統負載。比如當G中包含建立新協程的時候,M建立了G’,為了繼續執行G,需要把G’交給M’執行,也造成了很差的區域性性,因為G’和G是相關的,最好放在M上執行,而不是其他M'。
3. 系統呼叫(CPU在M之間的切換)導致頻繁的執行緒阻塞和取消阻塞操作增加了系統開銷。
GMP模型:
P的數量:
由啟動時環境變數`$GOMAXPROCS`或者是由`runtime`的方法`GOMAXPROCS()`決定
M的數量:
go語言本身的限制:go程式啟動時,會設定M的最大數量,預設10000.但是核心很難支援這麼多的執行緒數
runtime/debug中的SetMaxThreads函式,設定M的最大數量
一個M阻塞了,會建立新的M。
P何時建立:在確定了P的最大數量n後,執行時系統會根據這個數量建立n個P。
M何時建立:沒有足夠的M來關聯P並執行其中的可執行的G。比如所有的M此時都阻塞住了,而P中還有很多就緒任務,就會去尋找空閒的M,而沒有空閒的,就會去建立新的M。
全場景解析:
1.P擁有G1,M1獲取P後開始執行G1,G1建立了G2,為了區域性性G2優先加入到P1的本地佇列。
2.G1執行完成後,M上執行的goroutine切換為G0,G0負責排程時協程的切換。從P的本地佇列取G2,從G0切換到G2,並開始執行G2。實現了執行緒M1的複用。
3.假設每個P的本地佇列只能存4個G。G2要建立了6個G,前4個G(G3, G4, G5, G6)已經加入p1的本地佇列,p1本地佇列滿了。
4.G2在建立G7的時候,發現P1的本地佇列已滿,需要執行負載均衡(把P1中本地佇列中前一半的G,還有新建立G轉移到全域性佇列),這些G被轉移到全域性佇列時,會被打亂順序
5.G2建立G8時,P1的本地佇列未滿,所以G8會被加入到P1的本地佇列。
6.在建立G時,執行的G會嘗試喚醒其他空閒的P和M組合去執行。假定G2喚醒了M2,M2繫結了P2,並執行G0,但P2本地佇列沒有G,M2此時為自旋執行緒
7.M2嘗試從全域性佇列取一批G放到P2的本地佇列,至少從全域性佇列取1個g,但每次不要從全域性佇列移動太多的g到p本地佇列,給其他p留點。
8.假設G2一直在M1上執行,經過2輪後,M2已經把G7、G4從全域性佇列獲取到了P2的本地佇列並完成執行,全域性佇列和P2的本地佇列都空了,那m就要執行work stealing(偷取):從其他有G的P哪裡偷取一半G過來,放到自己的P本地佇列。P2從P1的本地佇列尾部取一半的G
9.G1本地佇列G5、G6已經被其他M偷走並執行完成,當前M1和M2分別在執行G2和G8,M3和M4沒有goroutine可以執行,M3和M4處於自旋狀態,它們不斷尋找goroutine。系統中最多有GOMAXPROCS個自旋的執行緒,多餘的沒事做執行緒會讓他們休眠。
10.假定當前除了M3和M4為自旋執行緒,還有M5和M6為空閒的執行緒,G8建立了G9,G8進行了阻塞的系統呼叫,M2和P2立即解綁,P2會執行以下判斷:如果P2本地佇列有G、全域性佇列有G或有空閒的M,P2都會立馬喚醒1個M和它繫結,否則P2則會加入到空閒P列表,等待M來獲取可用的p。
11.G8建立了G9,假如G8進行了非阻塞系統呼叫。M2和P2會解綁,但M2會記住P2,然後G8和M2進入系統呼叫狀態。當G8和M2退出系統呼叫時,會嘗試獲取P2,如果無法獲取,則獲取空閒的P,如果依然沒有,G8會被記為可執行狀態,並加入到全域性佇列,M2因為沒有P的繫結而變成休眠狀態
當執行緒M⽆可運⾏的G時,嘗試從其他M繫結的P偷取G,減少空轉,提高了執行緒利用率(避免閒著不幹活)。
當從本執行緒繫結 P 本地 佇列、全域性G佇列、netpoller都找不到可執行的 g,會從別的 P 裡竊取G並放到當前P上面。
從netpoller 中拿到的G是_Gwaiting狀態( 存放的是因為網路IO被阻塞的G),從其它地方拿到的G是_Grunnable狀態
從全域性佇列取的G數量:N = min(len(GRQ)/GOMAXPROCS + 1, len(GRQ/2)) (根據GOMAXPROCS負載均衡)
從其它P本地佇列竊取的G數量:N = len(LRQ)/2(平分)
也稱為P分離機制,當本執行緒 M 因為 G 進行的系統呼叫阻塞時,執行緒釋放繫結的 P,把 P 轉移給其他空閒的 M 執行,也提高了執行緒利用率(避免站著茅坑不拉shi)。
有 2 種方式可以檢視一個程式的排程GMP資訊,分別是go tool trace和GODEBUG
額,這個不太瞭解!
好的你回去等通知吧!
編譯器會根據變數是否被外部引用來決定是否逃逸:
如果函式外部沒有引用,則優先放到棧中;
如果函式外部存在引用,則必定放到堆中;
如果棧上放不下,則必定放到堆上;
案例:
指標逃逸:函式返回值為區域性變數的指標,函式雖然退出了,但是因為指標的存在,指向的記憶體不能隨著函式結束而回收,因此只能分配在堆上。
棧空間不足:當棧空間足夠時,不會發生逃逸,但是當變數過大時,已經完全超過棧空間的大小時,將會發生逃逸到堆上分配記憶體。區域性變數s佔用記憶體過大,編譯器會將其分配到堆上
變數大小不確定:編譯期間無法確定slice的長度,這種情況為了保證記憶體的安全,編譯器也會觸發逃逸,在堆上進行分配記憶體
動態型別:動態型別就是編譯期間不確定引數的型別、引數的長度也不確定的情況下就會發生逃逸
閉包引用物件:閉包函式中區域性變數i在後續函式是繼續使用的,編譯器將其分配到堆上
總結:
1. 棧上分配記憶體比在堆中分配記憶體效率更高
2. 棧上分配的記憶體不需要 GC 處理,而堆需要
3. 逃逸分析目的是決定內分配地址是棧還是堆
4. 逃逸分析在編譯階段完成
因為無論變數的大小,只要是指標變數都會在堆上分配,所以對於小變數我們還是使用傳值效率(而不是傳指標)更高一點。
什麼是記憶體對齊
為了能讓CPU可以更快的存取到各個欄位,Go編譯器會幫你把struct結構體做資料的對齊。所謂的資料對齊,是指記憶體地址是所儲存資料大小(按位元組為單位)的整數倍,以便CPU可以一次將該資料從記憶體中讀取出來。編譯器通過在結構體的各個欄位之間填充一些空白已達到對齊的目的。存在記憶體空間的浪費,實際上是空間換時間
對齊原則:
1. 結構體變數中成員的偏移量必須是成員大小的整數倍
2. 整個結構體的地址必須是最大位元組的整數倍
在應用程式中會使用到兩種記憶體,分別為堆(Heap)和棧(Stack),GC負責回收堆記憶體,而不負責回收棧中的記憶體
常用GC演算法
1.引用計數:python,swift,php
2.分代收集:Java
3.標記清除:GO 三色標記法+混合屏障 停頓時間在0.5ms左右
1.控制記憶體分配的速度,限制 Goroutine 的數量,提高賦值器 mutator 的 CPU 利用率(降低GC的CPU利用率)
2.少量使用+連線string
3.slice提前分配足夠的記憶體來降低擴容帶來的拷貝
4.避免map key物件過多,導致掃描時間增加
5.變數複用,減少物件分配,例如使用 sync.Pool 來複用需要頻繁建立臨時物件、使用全域性變數等
6.增大 GOGC 的值,降低 GC 的執行頻率 (不太用這個)
1. GODEBUG='gctrace=1' go run main.go
2. go tool trace trace.out
3. debug.ReadGCStats
4. runtime.ReadMemStats
額,這個不太瞭解!
好的你回去等通知吧!
go run -race main.go
好無聊的面試題,正常人誰這麼寫程式碼
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func dog(dogChan chan bool, catChan chan bool) { i := 0 for { select { case <-dogChan: fmt.Println("dog", i) i++ catChan <- true break default: break } } } func cat(catChan chan bool, fishChan chan bool) { for { select { case <-catChan: fmt.Println("cat") fishChan <- true break default: break } } } func fish(fishChan chan bool, dogChan chan bool) { i := 0 for { select { case <-fishChan: fmt.Println("fish") i++ // 計數,列印完之後就溜溜結束了。 if i > 9 { wg.Done() return } dogChan <- true break default: break } } } func main() { dogChan, catChan, fishChan := make(chan bool), make(chan bool), make(chan bool) wg.Add(1) go dog(dogChan, catChan) go cat(catChan, fishChan) go fish(fishChan, dogChan) dogChan <- true // 記得這裡進行啟動條件,不然就沒法啟動了。 wg.Wait() }
func main() {
a := [3]int{1, 2, 3}
for k, v := range a {
if k == 0 {
a[0], a[1] = 100, 200
}
a[k] = 100 + v
}
fmt.Print(a) //陣列 101 102 103
}
func main() {
a := []int{1, 2, 3}
for k, v := range a {
if k == 0 {
a[0], a[1] = 100, 200
}
a[k] = 100 + v
}
fmt.Print(a) //切片 101 300 103
}
package main
import "fmt"
func main() {
var a uint = 0
var b uint = 1
c := a - b
fmt.Print(c) //18446744073709551615 64位CPU 2^64-1 32位CPU 2^32-1
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結