過年前 再帶大家卷一波Go高質量知識點

發表於2024-02-11
以下內容來自 Go就業訓練營 的提問和答疑。

1. ⼀個T型別的值可以調⽤*T型別宣告的⽅法,當且僅當T是可定址的。(怎麼理解可定址)

可定址是指能夠獲取變數的記憶體地址。

在 Go 語言中,以下型別的值是可定址的:
  1. 值型別(Value Types):包括基本型別(如整數、浮點數、布林值等)和結構體(struct)型別。可以透過取地址運算子 & 來獲取變數的記憶體地址。
  2. 陣列(Array):陣列的元素是值型別,因此陣列的元素也是可定址的。
  3. 切片(Slice):切片是對陣列的引用,透過索引操作可以獲取切片中的元素的地址。
  4. 指標(Pointer):指標本身就是儲存變數記憶體地址的型別,因此指標是可定址的。
以下型別的值是不可定址的:
  1. 常量(Constants):常量是不可變的,因此沒有記憶體地址。
  2. 字串(String):字串是不可變的,因此沒有記憶體地址。
  3. 字面量(Literals):字面量是直接使用的常量值,沒有對應的變數,因此沒有記憶體地址。

理解可定址的概念對於理解 Go 語言中方法的呼叫和接收者的限制非常重要:只有當一個型別是可定址的,才能夠呼叫該型別的指標接收者方法。這是因為指標接收者方法需要在方法內部修改接收者的狀態,而只有可定址的值才能被修改。

補充說明字串的特點

  1. 在 Go 語言中,字串是不可變的,這意味著字串的值在建立後是不可修改的。雖然字串的底層是位元組陣列,但是字串的不可變性是由 Go 語言的設計決策所決定的。
  2. 字串的不可定址性是指不能直接透過索引或指標來修改字串中的某個字元。雖然字串的底層是位元組陣列,但是字串值本身是隻讀的,無法透過修改底層位元組陣列來修改字串的值。這是為了確保字串的不可變性和安全性。
  3. 因此,字串在語言層面上被視為不可定址的,無法直接修改其中的字元。

2. 三⾊標記法若不被STW保護可能會導致物件丟失,⽩⾊物件被⿊⾊物件引⽤,灰⾊物件對⽩⾊物件的引⽤丟失(為什麼需要這個條件),導致物件丟失。

  1. 三色標記法是一種用於垃圾回收的演算法,用於標記和回收不再使用的物件。在三色標記法中,物件被標記為三種不同的顏色:白色、灰色和黑色。
  2. 在垃圾回收過程中,白色物件表示未被訪問的物件,灰色物件表示正在被訪問的物件,黑色物件表示已經被訪問並且是可達的物件。
  3. 在三色標記法中,灰色物件對白色物件的引用是非常重要的。這是因為灰色物件表示正在被訪問的物件,如果灰色物件對白色物件的引用丟失,那麼這個白色物件將無法被訪問到,也就無法被正確地標記為可達的物件。如果灰色物件對白色物件的引用丟失,那麼在垃圾回收過程中,這個白色物件將被錯誤地標記為不可達的物件,從而導致物件丟失。這可能會導致記憶體洩漏或錯誤地回收仍然可達的物件。
  4. 建議你再好好看下這個教程:[Golang三色標記混合寫屏障GC模式全分析
    ](https://learnku.com/articles/68141)
  5. 有哪些不理解的可以看b站對應的影片教程,第9章之後只看文件理解起來可能還是有些吃力的:Golang GC詳解影片

3. 逃逸分析相關的問題,堆有時需要加鎖:堆上的記憶體,有時需要加鎖防⽌多執行緒衝突,為什麼堆上的記憶體有時需要加鎖?⽽不是⼀直需要加鎖呢?

  1. 堆上的記憶體有時需要加鎖是因為堆上的記憶體是被多個執行緒共享的,當多個執行緒同時訪問和修改堆上的記憶體時,可能會發生併發衝突。 為了保證資料的一致性和避免競態條件,需要對堆上的記憶體進行加鎖。
  2. 然而,並不是所有情況下都需要對堆上的記憶體進行加鎖。加鎖會引入額外的開銷,並且可能導致效能下降。
  3. 我們可以透過其他方式來避免對堆上的記憶體進行加鎖。例如,可以使用無鎖資料結構、使用分段鎖或使用事務等技術來減少對堆上記憶體的競爭,提高併發效能。這些方法可以透過設計合理的資料結構和演算法來避免併發衝突,從而不需要對堆上的記憶體進行加鎖。
  4. 最後總結一下:是否需要對堆上的記憶體進行加鎖取決於具體的併發場景和需求。在一些情況下,可以透過其他方式來避免對堆上記憶體的競爭,提高併發效能。只有在確實存在併發衝突的情況下,才需要對堆上的記憶體進行加鎖來保證資料的一致性和避免競態條件。

為了讓你更好的理解,我再補充一下:

下面這些情況就不需要對堆上的記憶體加鎖:
  1. 只讀操作: 如果多個執行緒只是對堆上的記憶體進行讀取操作,並且沒有寫操作,那麼不需要對堆上的記憶體進行加鎖。只讀操作不會引起併發衝突,因此不需要額外的同步措施。
  2. 無共享狀態: 如果多個執行緒之間沒有共享狀態,即它們訪問的是獨立的堆上記憶體,那麼也不需要對堆上的記憶體進行加鎖。每個執行緒操作的是自己獨立的記憶體,不存在併發衝突的問題。
  3. 無競爭條件: 如果多個執行緒對堆上的記憶體進行操作,但它們之間沒有競爭條件,即它們的操作不會相互干擾或產生不一致的結果,那麼也不需要對堆上的記憶體進行加鎖。
  4. 使用無鎖資料結構: 如果使用了無鎖資料結構,例如原子操作、無鎖佇列等,這些資料結構本身已經提供了併發安全的操作,不需要額外的加鎖。
  5. 以上這些提供了思考的角度,具體是否需要對堆上的記憶體進行加鎖取決於具體的併發場景和需求。

4. 堆記憶體具體是如何分配的,由誰持有?mcache :執行緒快取,mcentral :中央快取,mheap :堆記憶體,執行緒快取 mcache?

在 Go 語言中,堆記憶體的分配是由 Go 執行時(runtime)負責管理的。

下面是堆記憶體分配的一般過程:

  1. 執行緒快取(mcache):每個邏輯處理器(P)都有一個執行緒快取(mcache),用於儲存一些預分配的記憶體塊。執行緒快取是每個執行緒獨立擁有的,用於提高記憶體分配的效能。
  2. 中央快取(mcentral):中央快取是全域性共享的,用於儲存更多的記憶體塊。當執行緒快取不足時,會從中央快取獲取更多的記憶體塊。
  3. 堆記憶體(mheap):如果中央快取也沒有足夠的記憶體塊,Go 執行時會從堆記憶體中獲取更多的記憶體。堆記憶體是用於儲存動態分配的物件的區域。

在堆記憶體的分配過程中,執行緒快取(mcache)被邏輯處理器(P)持有。每個邏輯處理器都有自己的執行緒快取,用於儲存預分配的記憶體塊。當執行緒快取不足時,邏輯處理器會從中央快取(mcentral)獲取更多的記憶體塊。如果中央快取也不足,邏輯處理器會從堆記憶體(mheap)中獲取更多的記憶體。

透過這種機制,Go 執行時可以高效地管理和分配堆記憶體,同時減少對全域性鎖的競爭。每個邏輯處理器都有自己的執行緒快取,從而減少了對共享資源的競爭,提高了併發效能。

5. 編譯器透過逃逸分析去選擇記憶體分配到堆或者棧? ⽣命週期不可知的情況有哪些?是發生指標逃逸嗎?

  1. 首先第一個提問的理解是正確的。
生命週期不可知的情況包括:
  1. 指標逃逸:當一個指標被返回給函式的呼叫者、儲存在全域性變數中或逃逸到堆上時,編譯器無法確定指標的生命週期。
  2. 閉包:當一個函式內部定義的閉包引用了外部的變數,並且這個閉包被返回、儲存在全域性變數中或逃逸到堆上時,編譯器無法確定閉包的生命週期。
  3. 併發程式設計:在併發程式設計中,如果一個變數被多個 Goroutine 共享訪問,並且可能在 Goroutine 之間傳遞或逃逸到堆上時,編譯器無法確定變數的生命週期。

6. 如果生命週期可知,則一定在棧上分配嗎? 如果不可知,則認為記憶體逃逸,必須在堆上分配?

  1. 如果變數的生命週期是完全可知的,編譯器會優先將其分配在棧上。 這是因為棧上的記憶體分配和釋放是非常高效的,僅僅需要移動棧指標即可完成。棧上的記憶體分配是自動管理的,當變數超出作用域時,棧上的記憶體會自動釋放。
  2. 注意:並不是所有生命週期可知的變數都一定在棧上分配。 編譯器可能會根據一些其他的因素來決定記憶體的分配位置。例如,如果變數的大小較大,超過了棧的限制,編譯器可能會選擇在堆上分配記憶體。
  3. 後面的提問你的理解是正確的:如果變數的生命週期不可知,編譯器會認為它會逃逸到堆上,並在堆上進行分配。逃逸到堆上的變數可以在函式返回後繼續被訪問,或者被其他函式或 Goroutine 引用。在這種情況下,編譯器無法確定變數的生命週期,因此選擇在堆上分配記憶體。
  4. 注意:編譯器的具體實現可能會有所不同,不同的編譯器可能會有不同的策略來處理記憶體的分配。因此,雖然生命週期可知的變數通常會在棧上分配,但並不是絕對的規則。編譯器會根據具體情況進行最佳化和決策,以提高程式的效能和效率。

7. mutex和原⼦鎖混⽤導致mutex失效的情況和原因?

  1. 重複加鎖和解鎖:如果在使用 Mutex 和原子鎖時混淆了它們的使用,可能會導致重複加鎖和解鎖的問題。例如,使用 Mutex 加鎖後又使用原子鎖進行操作,然後再次使用 Mutex 解鎖。這種混亂的加鎖和解鎖順序可能導致互斥鎖的狀態不一致,從而使 Mutex 失去了正確的同步效果。
  2. 未正確保護共享資源:Mutex 和原子鎖的目的是保護共享資源的訪問,但如果在使用它們時沒有正確地保護共享資源,也會導致 Mutex 失效。例如,使用 Mutex 加鎖後,但在訪問共享資源時使用了原子操作而沒有使用 Mutex 進行保護,這樣其他執行緒可能會在沒有正確同步的情況下訪問共享資源。
  3. 不一致的同步策略:Mutex 和原子鎖是不同的同步機制,它們有不同的語義和使用方式。如果在同一個程式碼塊或函式中混用了 Mutex 和原子鎖,可能會導致不一致的同步策略。例如,一個執行緒使用 Mutex 加鎖後,另一個執行緒使用原子鎖進行操作,這樣就無法保證正確的同步和互斥訪問。
  4. 競態條件和資料競爭:混用 Mutex 和原子鎖時,如果沒有正確地處理競態條件和資料競爭,也會導致 Mutex 失效。競態條件是指多個執行緒對共享資源的訪問順序不確定,可能導致不一致的結果。資料競爭是指多個執行緒同時訪問和修改共享資源,可能導致資料的不確定性和不一致性。
    總結一下也就是:在併發程式設計中,需要遵循一致的同步策略,正確地使用 Mutex 或原子鎖來保護共享資源的訪問。混用 Mutex 和原子鎖時,需要確保加鎖和解鎖的順序正確,並保證所有對共享資源的訪問都經過正確的同步機制。

8. Go語言互斥鎖的問題,解鎖後會發出訊號量通知阻塞的協程:若有多個協程阻塞,如何保證只有⼀個協程被喚

醒?若存在飢餓模式如何保證處於飢餓模式的協程優先獲得鎖?

  1. 在互斥鎖的實現中,解鎖後會發出訊號量通知阻塞的協程,但是並不能保證只有一個協程被喚醒。多個協程可能同時被喚醒,然後競爭互斥鎖。這是因為互斥鎖的喚醒是由作業系統的排程器來控制的,排程器可能會同時喚醒多個協程。
  2. 為了確保只有一個協程被喚醒,可以結合條件變數(Cond)來實現。條件變數可以與互斥鎖一起使用,透過條件變數的 Wait() 和 Signal() 或 Broadcast() 方法來實現協程的喚醒和等待。當互斥鎖解鎖時,可以使用條件變數的 Signal() 方法來喚醒一個協程,或使用 Broadcast() 方法喚醒所有協程。這樣可以確保只有一個或一組協程被喚醒,其他協程仍然保持阻塞狀態。
  3. 對於飢餓模式,可以使用公平鎖(Fair Mutex)來解決。我在下面給你寫了一個程式碼示例:我們自定義了一個 FairMutex 結構體,其中使用了 sync.Cond 來實現條件變數。公平鎖會按照請求的順序來分配鎖,確保等待時間最長的協程優先獲得鎖,從而避免飢餓問題。
  4. 在 FairMutex 的 Lock() 方法中,使用了 for 迴圈來等待鎖的釋放,確保只有一個協程被喚醒。當鎖被解鎖時,使用條件變數的 Signal() 方法來喚醒一個協程,而其他協程仍然保持阻塞狀態。
示例程式碼
package main  
  
import (  
"fmt"  
"sync"  
)  
  
type FairMutex struct {  
mu sync.Mutex  
cond *sync.Cond  
isLocked bool  
}  
  
func NewFairMutex() *FairMutex {  
return &FairMutex{  
cond: sync.NewCond(&sync.Mutex{}),  
isLocked: false,  
}  
}  
  
func (fm *FairMutex) Lock() {  
fm.mu.Lock()  
defer fm.mu.Unlock()  
  
for fm.isLocked {  
fm.cond.Wait()  
}  
fm.isLocked = true  
}  
  
func (fm *FairMutex) Unlock() {  
fm.mu.Lock()  
defer fm.mu.Unlock()  
  
fm.isLocked = false  
fm.cond.Signal()  
}  
  
func main() {  
fm := NewFairMutex()  
  
var wg sync.WaitGroup  
for i := 0; i < 5; i++ {  
wg.Add(1)  
go func(id int) {  
fm.Lock()  
defer fm.Unlock()  
  
fmt.Printf("Goroutine %d acquired the lock\n", id)  
// Do some work...  
fmt.Printf("Goroutine %d released the lock\n", id)  
  
wg.Done()  
}(i)  
}  
  
wg.Wait()  
}  
執行結果

歡迎關注我

本文首發公眾號:王中陽Go

加我微信邀你進 就業跳槽交流群 :wangzhongyang1993

新年快樂

最後祝大家新年快樂,龍年大吉,放假了好好休息,我們們明年繼續戰鬥!

相關文章