面試某大廠,被Channel給吊打了,這次一次性通關channel!

qi66發表於2023-04-13

一 前言

前幾天面試某大廠的雲原生崗位,原本是一個輕鬆+愉快的過程,當問到第二個問題,我就發現事情的不對勁,先覆盤一下面試官有關Channel的問題,然後再逐一解決,最後進行擴充套件,這次一定要一次性通關channel!答應我,看完這篇文章,不要再被Channel吊打了!

面試題

  1. 介紹一下Channel

  2. Channel在go中起什麼作用

  3. Channel為什麼需要兩個佇列實現

  4. Go為什麼要開發Channel,而別的語言為什麼沒有

  5. Channel底層是使用鎖控制併發的,為什麼不直接使用鎖

然後我們進行一下擴充套件,玩轉Channel!

  1. Channel的底層原理和資料結構

  2. Channel的讀寫流程

  3. Channel為什麼能做到執行緒安全

  4. 操作Channel可能出現的情況

  5. Channel有哪些常見的使用場景

  6. Channel的讀寫操作是否是原子性的,如何實現

  7. 如何避免在Channel中出現死鎖的情況

  8. Channel可以在多個goroutine之間傳遞什麼型別的資料

  9. 如何在Channel中使用快取區

  10. 在使用Channel時,如何保證資料的同步性和一致性

  11. 如何保證Channel的安全性

  12. Channel的大小是否對效能有影響

  13. Channel的記憶體模型是什麼

  14. 如何在Channel中傳遞複雜的資料型別

  15. Channel和goroutine之間的關係是什麼

  16. 在Go語言中,Channel和鎖的使用場景有哪些區別

二 解決面試題

1. 介紹一下Channel

Channel是Go語言中的一種併發原語,用於在goroutine之間傳遞資料和同步執行。Channel實際上是一種特殊型別的資料結構,可以將其想象成一個管道,透過它可以傳送和接收資料,實現goroutine之間的通訊和同步。

Channel的特點包括:

  1. Channel是型別安全的,可以確保傳送和接收的資料型別一致。
  2. Channel是阻塞的,當傳送或接收操作沒有被滿足時,會阻塞當前goroutine,直到滿足條件。
  3. Channel是有快取的,可以指定快取區大小,當快取區已滿時傳送操作會被阻塞,當快取區為空時接收操作會被阻塞。
  4. Channel是可以關閉的,可以使用close()函式關閉Channel,關閉後的Channel不能再進行傳送操作,但可以進行接收操作。

Channel的使用方式包括:

  1. 建立Channel:使用make()函式建立Channel,指定Channel的型別和快取區大小。
  2. 傳送資料:使用<-運運算元將資料傳送到Channel中。
  3. 接收資料:使用<-運運算元從Channel中接收資料。
  4. 關閉Channel:使用close()函式關閉Channel。

2. Channel在go中起什麼作用

在 Go 中,channel 是一種用於在 goroutine 之間傳遞資料的併發原語。channel 可以讓 goroutine 在傳送和接收操作之間同步,從而避免了競態條件,從而更加安全地共享記憶體。

channel 類似於一個佇列,資料可以從一個 goroutine 中傳送到 channel,然後從另一個 goroutine 中接收。channel 可以是有緩衝的,這意味著可以在 channel 中儲存一定數量的值,而不僅僅是一個。如果 channel 是無緩衝的,則傳送和接收操作將會同步阻塞,直到有 goroutine 準備好接收或傳送資料。

注:我這裡提到了Channel底層用到了兩個佇列實現。所以就有了下面的問題

3. Channel為什麼需要兩個佇列實現

一個Channel可以被看作是一個通訊通道,用於在不同的程式之間傳遞資料。在具體的實現中,一個Channel通常需要使用兩個佇列來實現。這兩個佇列是傳送佇列和接收佇列。

傳送佇列是用來儲存將要傳送的資料的佇列。當一個程式想要透過Channel傳送資料時,它會將資料新增到傳送佇列中。傳送佇列中的資料會按照先進先出的順序被逐個傳送到接收程式。如果傳送佇列已經滿了,那麼傳送程式就需要等待,直到有足夠的空間可以儲存資料。

接收佇列是用來儲存接收程式已經準備好接收的資料的佇列。當一個程式從Channel中接收資料時,它會從接收佇列中取出資料。如果接收佇列是空的,那麼接收程式就需要等待,直到有新的資料可以接收。

使用兩個佇列實現Channel的主要原因是為了實現非同步通訊。傳送程式可以在傳送資料之後立即繼續執行其他任務,而不需要等待接收程式確認收到資料。同樣,接收程式也可以在等待資料到達的同時執行其他任務。這種非同步通訊的實現方式可以提高系統的吞吐量和響應速度。

4. Go為什麼要開發Channel,而別的語言為什麼沒有

在Go語言中,Channel是一種非常重要的併發原語。Go語言將Channel作為語言內建的原語,可能是出於以下幾個方面的考慮:

  1. 併發安全:在多執行緒併發環境下,使用Channel可以保證資料的安全性,避免多個執行緒同時訪問共享資料導致的資料競爭和鎖的開銷。
  2. 簡單易用:Go語言中的Channel是一種高度抽象的概念,可以非常方便地實現不同執行緒之間的資料傳輸和同步。透過Channel,程式設計師不需要手動地管理鎖、條件變數等底層的同步原語,使得程式的編寫更加簡單和高效。
  3. 天然支援併發:Go語言中的Channel與goroutine密切相關,這使得Channel天然地支援併發。程式設計師可以透過使用Channel和goroutine來實現非常高效的併發程式設計。

雖然其他程式語言中沒有像Go語言中的Channel這樣的內建併發原語,但是許多程式語言提供了類似於Channel的實現,比如Java的ConcurrentLinkedQueue、Python的Queue、C++的std::queue等。這些實現雖然沒有Go語言中的Channel那麼簡單易用和高效,但也能夠滿足多執行緒程式設計中的資料傳輸和同步需求。

注:我這裡提到了Channel底層是使用鎖實現。所以就有了下面的問題

5. Channel底層是使用鎖控制併發的,為什麼不直接使用鎖

雖然在Go語言中,Channel底層實現是使用鎖控制併發的,但是Channel和鎖的使用場景是不同的,具有不同的優勢和適用性。

首先,Channel比鎖更加高階和抽象。Channel可以實現多個goroutine之間的同步和資料傳遞,不需要程式設計師顯式地使用鎖來進行執行緒間的協調。Channel可以避免常見的同步問題,比如死鎖、飢餓等問題。

其次,Channel在語言層面提供了一種更高效的併發模型。在使用鎖進行併發控制時,需要程式設計師自己手動管理鎖的獲取和釋放,這增加了程式碼複雜度和錯誤的風險。而使用Channel時,可以透過goroutine的排程和Channel的阻塞機制來實現更加高效和簡單的併發控制。

此外,Channel還可以避免一些由鎖導致的效能問題,如鎖競爭、鎖粒度過大或過小等問題。Channel提供了一種更加精細的控制機制,能夠更好地平衡不同goroutine之間的併發效能。

總的來說,雖然Channel底層是使用鎖控制併發的,但是Channel在語言層面提供了更加高階、抽象和高效的併發模型,可以使程式設計師更加方便和安全地進行併發程式設計。

三 擴充套件面試題

1. Channel的底層原理和資料結構

在Go語言中,Channel是透過一個有快取的佇列來實現的,底層資料結構是一個雙向連結串列。是一個叫做hchan的結構體,每個Channel都有一個send佇列和一個receive佇列,用於存放傳送和接收操作的goroutine。當傳送操作和接收操作發生時,它們會被新增到對應的佇列中,等待對方的操作來滿足條件。

type hchan struct {
  //channel分為無緩衝和有緩衝兩種。
  //對於有緩衝的channel儲存資料,藉助的是如下迴圈陣列的結構
	qcount   uint           // 迴圈陣列中的元素數量
	dataqsiz uint           // 迴圈陣列的長度
	buf      unsafe.Pointer // 指向底層迴圈陣列的指標
	elemsize uint16 //能夠收發元素的大小
  
	closed   uint32   //channel是否關閉的標誌
	elemtype *_type //channel中的元素型別
  
  //有緩衝channel內的緩衝陣列會被作為一個“環型”來使用。
  //當下標超過陣列容量後會回到第一個位置,所以需要有兩個欄位記錄當前讀和寫的下標位置
	sendx    uint   // 下一次傳送資料的下標位置
	recvx    uint   // 下一次讀取資料的下標位置
  
  //當迴圈陣列中沒有資料時,收到了接收請求,那麼接收資料的變數地址將會寫入讀等待佇列
  //當迴圈陣列中資料已滿時,收到了傳送請求,那麼傳送資料的變數地址將寫入寫等待佇列
	recvq    waitq  // 讀等待佇列
	sendq    waitq  // 寫等待佇列

	lock mutex //互斥鎖,保證讀寫channel時不存在併發競爭問題
}

對於有快取的Channel,快取區的大小即為佇列的長度,當快取區已滿時,傳送操作會被阻塞,直到有接收操作來取走資料;當快取區為空時,接收操作會被阻塞,直到有傳送操作來填充資料。

Channel底層的同步機制是基於等待佇列和訊號量實現的。每個Channel都維護著一個等待佇列,其中包含了所有等待操作的goroutine;同時還維護著一個計數器,用於記錄當前快取區中的元素數量。當傳送操作需要等待時,會將當前goroutine新增到等待佇列中,並使計數器減一;當接收操作需要等待時,會將當前goroutine新增到等待佇列中,並使計數器加一。當有其他操作滿足條件時,會從等待佇列中取出相應的goroutine,並將其重新加入到可執行佇列中,等待排程器的排程。

2. Channel的讀寫流程

向 channel 寫資料:

若等待接收佇列 recvq 不為空,則緩衝區中無資料或無緩衝區,將直接從 recvq 取出 G ,並把資料寫入,最後把該 G 喚醒,結束髮送過程。

若緩衝區中有空餘位置,則將資料寫入緩衝區,結束髮送過程。

若緩衝區中沒有空餘位置,則將傳送資料寫入 G,將當前 G 加入 sendq ,進入睡眠,等待被讀 goroutine 喚醒。

從 channel 讀資料

若等待傳送佇列 sendq 不為空,且沒有緩衝區,直接從 sendq 中取出 G ,把 G 中資料讀出,最後把 G 喚醒,結束讀取過程。

如果等待傳送佇列 sendq 不為空,說明緩衝區已滿,從緩衝區中首部讀出資料,把 G 中資料寫入緩衝區尾部,把 G 喚醒,結束讀取過程。

如果緩衝區中有資料,則從緩衝區取出資料,結束讀取過程。

將當前 goroutine 加入 recvq ,進入睡眠,等待被寫 goroutine 喚醒。

關閉 channel

1.關閉 channel 時會將 recvq 中的 G 全部喚醒,本該寫入 G 的資料位置為 nil。將 sendq 中的 G 全部喚醒,但是這些 G 會 panic。

panic 出現的場景還有:

  • 關閉值為 nil 的 channel
  • 關閉已經關閉的 channel
  • 向已經關閉的 channel 中寫資料

3. Channel為什麼能做到執行緒安全

Channel的執行緒安全主要是透過其內部的同步機制實現的。

Channel 可以理解是一個先進先出的佇列,透過管道進行通訊,傳送一個資料到Channel和從Channel接收一個資料都是原子性的。不要透過共享記憶體來通訊,而是透過通訊來共享記憶體,前者就是傳統的加鎖,後者就是Channel。設計Channel的主要目的就是在多工間傳遞資料的,本身就是安全的。

當多個goroutine透過Channel進行通訊時,Channel會保證每個操作的原子性和順序性,避免了多個goroutine同時訪問共享變數導致的資料競爭問題。Channel的阻塞特性也保證了在傳送和接收操作發生時,它們會被新增到等待佇列中,直到滿足條件後才會被喚醒,從而避免了死鎖問題。

4. 操作Channel可能出現的情況

channel存在3種狀態:

  • nil,未初始化的狀態,只進行了宣告,或者手動賦值為nil
  • active,正常的channel,可讀或者可寫
  • closed,已關閉,千萬不要誤認為關閉channel後,channel的值是nil
操作 一個零值nil通道 一個非零值但已關閉的通道 一個非零值且尚未關閉的通道
關閉 產生恐慌 產生恐慌 成功關閉
傳送資料 永久阻塞 產生恐慌 阻塞或者成功傳送
接收資料 永久阻塞 永不阻塞 阻塞或者成功接收

5. Channel有哪些常見的使用場景

  1. 任務分發和處理:可以透過Channel將任務分發給多個goroutine進行處理,並將處理結果傳送回主goroutine進行彙總和處理。
  2. 併發控制:可以透過Channel來進行訊號量控制,限制併發的數量,避免資源競爭和死鎖等問題。
  3. 資料流處理:可以透過Channel實現資料流的處理,將資料按照一定的規則傳遞給不同的goroutine進行處理,提高併發處理效率。
  4. 事件通知和處理:可以透過Channel來實現事件的通知和處理,將事件傳送到Channel中,讓訂閱了該Channel的goroutine進行相應的處理。
  5. 非同步處理:可以透過Channel實現非同步的處理,將任務交給其他goroutine處理,自己繼續執行其他任務,等待處理結果時再從Channel中獲取。

6. Channel的讀寫操作是否是原子性的,如何實現

Channel的讀寫操作是原子性的,並且是由Go語言內部的同步機制來保證的。

當一個goroutine進行Channel的讀寫操作時,Go語言內部會自動進行同步,保證該操作的原子性和順序性。這種同步機制主要涉及到兩個部分:

  1. 基於鎖的同步:在Channel的底層實現中,使用了一種基於鎖的同步機制,它可以保證每個讀寫操作都是原子性的,避免了多個goroutine同時讀寫導致的資料競爭問題。
  2. 基於等待的同步:當一個goroutine進行Channel的讀寫操作時,如果Channel當前為空或已滿,它就會被新增到等待佇列中,直到滿足條件後才會被喚醒,這種等待的同步機制可以避免因Channel狀態不滿足條件而導致的死鎖問題。

透過這種基於鎖和等待的同步機制,Go語言保證了Channel的讀寫操作是原子性的,可以在多個goroutine之間安全地進行通訊和同步。

7. 如何避免在Channel中出現死鎖的情況

  1. 避免在單個goroutine中對Channel進行讀寫操作:如果一個goroutine同時進行Channel的讀寫操作,很容易出現死鎖的情況,因為該goroutine無法切換到其他任務,導致無法釋放Channel的讀寫鎖。因此,在進行Channel的讀寫操作時,應該儘量將它們分配到不同的goroutine中,以便能夠及時切換任務。
  2. 使用緩衝Channel:緩衝Channel可以在一定程度上緩解讀寫操作的同步問題,避免因為Channel狀態不滿足條件而導致的死鎖問題。如果Channel是非緩衝的,那麼寫操作必須等到讀操作執行之後才能完成,反之亦然,這種同步會導致程式無法繼續執行。而如果使用緩衝Channel,就可以避免這種同步問題,即使讀寫操作之間存在時間差,也不會導致死鎖。
  3. 使用select語句:select語句可以在多個Channel之間進行選擇操作,避免因為某個Channel狀態不滿足條件而導致的死鎖問題。在使用select語句時,應該注意判斷每個Channel的狀態,避免出現同時等待多個Channel的情況,這可能導致死鎖。
  4. 使用超時機制:在進行Channel的讀寫操作時,可以設定一個超時時間,避免因為Channel狀態不滿足條件而一直等待的情況。如果超過一定時間仍然無法讀寫Channel,就可以選擇放棄或者進行其他操作,以避免死鎖。

8. Channel可以在多個goroutine之間傳遞什麼型別的資料

在Go語言中,Channel可以在多個goroutine之間傳遞任何型別的資料,包括基本資料型別、複合資料型別、結構體、自定義型別等。這些資料型別在傳遞過程中都會被封裝成對應的指標型別,並由Channel進行傳遞。

9. 如何在Channel中使用快取區

在Go語言中,我們可以使用帶緩衝的Channel來實現Channel的快取區功能。帶緩衝的Channel可以儲存一定數量的元素,而不必立即將它們交給接收方。這樣可以減少傳送和接收操作之間的同步,從而提高程式的效能。

使用帶緩衝的Channel,可以透過在Channel宣告時指定緩衝區的大小來實現。例如,宣告一個容量為10的緩衝Channel可以使用以下語句:

ch := make(chan int, 10)

在這個例子中,我們建立了一個整型緩衝Channel,其容量為10。這意味著在Channel中可以儲存10個整型元素,而不必立即將它們傳送到接收方。當Channel中的元素數量達到緩衝區容量時,再進行寫入操作時,寫入操作就會被阻塞,直到有接收方讀取了Channel中的元素。

10. 在使用Channel時,如何保證資料的同步性和一致性

在使用Channel時,為了保證資料的同步性和一致性,可以採用以下幾種方式:

  1. 合理設計Channel的容量:當Channel容量過小時,容易出現傳送者和接收者之間的阻塞,而當容量過大時,可能會出現資料不一致的問題。因此,在設計Channel時,需要根據實際情況合理設定容量大小,以避免資料同步性和一致性的問題。
  2. 使用互斥鎖保證資料訪問的互斥性:如果多個goroutine同時對某個共享的資料進行訪問,可能會導致資料不一致的問題。此時,可以使用互斥鎖來保證資料訪問的互斥性,以避免多個goroutine同時對同一份資料進行訪問。
  3. 使用同步機制實現資料同步:在某些情況下,我們可能需要在多個goroutine之間進行資料同步,以確保資料的一致性。此時,可以使用一些同步機制,例如WaitGroup、Barrier、Cond等,來實現資料同步。

11. 如何保證Channel的安全性

  1. 確保Channel的正確使用:在使用Channel時,需要確保傳送和接收操作的正確性。特別是在併發環境下,必須正確處理併發操作,避免出現競爭條件或死鎖等問題。因此,在使用Channel時,需要根據實際情況選擇合適的同步機制,例如互斥鎖、條件變數、原子操作等,以確保Channel的正確使用。
  2. 避免Channel的洩漏:如果Channel沒有被及時關閉,可能會導致資源洩漏和效能問題。因此,在使用Channel時,需要確保及時關閉Channel,避免出現資源洩漏的情況。
  3. 避免Channel的阻塞:如果Channel的容量較小,可能會導致傳送和接收操作的阻塞。此時,可以使用緩衝Channel或者帶超時的傳送和接收操作,避免Channel的阻塞。
  4. 避免Channel的死鎖:如果多個goroutine之間出現死鎖,可能會導致程式的停滯和效能問題。因此,在使用Channel時,需要避免死鎖的情況,例如避免迴圈依賴、避免同時使用多個Channel等。

12. Channel的大小是否對效能有影響

Channel的大小對效能會產生一定的影響。Channel的大小是指Channel可以容納的元素數量,可以透過在建立Channel時指定容量大小來控制。當Channel的容量較小時,可能會導致傳送和接收操作的阻塞,從而影響程式的效能。而當Channel的容量較大時,可能會增加系統的記憶體開銷,也可能會導致Channel中的元素被佔用的時間較長,從而影響程式的響應性。

13. Channel的記憶體模型是什麼

在Go語言中,Channel的記憶體模型是基於通訊順序程式(Communicating Sequential Processes,CSP)模型的。CSP模型是一種併發計算模型,它將併發程式看作是一組順序程式,這些程式透過Channel進行通訊和同步。

在CSP模型中,每個程式都是獨立的,它們之間透過Channel進行通訊。Channel是一個具有FIFO特性的資料結構,用於在多個程式之間傳遞資料。當一個程式向Channel傳送資料時,它會阻塞等待,直到另一個程式從Channel中接收到資料。同樣地,當一個程式從Channel中接收資料時,它也會阻塞等待,直到另一個程式向Channel傳送資料。

在Go語言中,Channel的記憶體模型採用了CSP模型的概念,即每個Channel都是一個獨立的順序程式。當一個程式向Channel傳送資料時,資料會被複制到Channel的緩衝區或者直接傳送到接收方。當一個程式從Channel中接收資料時,資料會被從Channel的緩衝區中取出或者等待傳送方傳送資料。

14. 如何在Channel中傳遞複雜的資料型別

在Go語言中,Channel可以傳遞任何型別的資料,包括複雜的資料型別。如果要在Channel中傳遞複雜的資料型別,可以將其定義為一個結構體,然後透過Channel進行傳遞。

例如,假設我們有一個結構體型別Person,它包含姓名和年齡兩個欄位:

type Person struct {
    Name string
    Age  int
}

我們可以定義一個Channel,用於傳遞Person型別的資料:

ch := make(chan Person)

現在我們可以在不同的Goroutine中向Channel傳送和接收Person型別的資料:

// 傳送Person型別資料到Channel
go func() {
    p := Person{Name: "Alice", Age: 18}
    ch <- p
}()

// 從Channel接收Person型別資料
p := <-ch
fmt.Println(p.Name, p.Age)

注意,如果要在Channel中傳遞複雜的資料型別,需要確保該型別是可匯出的。

15. Channel和goroutine之間的關係是什麼

在Go語言中,Channel和Goroutine是密切相關的,它們可以說是Go語言併發程式設計的兩個重要元件。

Goroutine是Go語言中輕量級的執行緒實現,可以在一個程式中建立成千上萬個Goroutine,並且它們的建立和銷燬的代價非常小,因此非常適合在高併發的場景下使用。Goroutine的排程是由Go執行時系統(runtime)負責的,它採用協作式排程,可以自動地在多個執行緒之間切換,以達到高效利用CPU的目的。

Channel是Goroutine之間通訊的一種方式,它可以用於在不同的Goroutine之間傳遞資料。Channel提供了兩個基本操作:傳送和接收。透過向Channel傳送資料,一個Goroutine可以將資料傳遞給另一個Goroutine;透過從Channel接收資料,一個Goroutine可以獲取其他Goroutine傳遞過來的資料。

因此,可以說Channel和Goroutine之間是一種協作關係:Goroutine可以透過Channel與其他Goroutine進行通訊,以實現協作和共享資料,從而完成複雜的併發任務。同時,Channel的實現也依賴於Goroutine和Go執行時系統,它們共同構成了Go語言併發程式設計的基礎。

16. 在Go語言中,Channel和鎖的使用場景有哪些區別

在Go語言中,Channel和鎖(sync.Mutex等)都可以用於併發程式設計中的同步和共享資料,但它們的使用場景有一些區別。

Channel通常用於Goroutine之間傳遞資料,併發的Goroutine之間可以透過Channel進行同步。使用Channel可以避免鎖的問題,例如死鎖、飢餓等問題。Channel可以將資料在多個Goroutine之間進行傳遞和共享,而且在資料傳遞的過程中,不需要使用鎖來保證資料的安全性,這也是Channel比鎖更加安全和高效的原因之一。因此,當需要在不同的Goroutine之間傳遞資料時,使用Channel是比較合適的選擇。

鎖通常用於對共享資源進行保護,防止多個Goroutine同時訪問和修改同一個共享資源,從而導致資料的競爭和不一致。使用鎖可以保證同一時刻只有一個Goroutine能夠訪問和修改共享資源,從而保證資料的安全性和一致性。當需要對共享資源進行保護時,使用鎖是比較合適的選擇。

Channel和鎖都是Go語言中常用的併發程式設計工具,它們各自有不同的使用場景。在實際開發中,應根據具體的需求選擇合適的併發程式設計工具來實現同步和共享資料。

四 最後

透過這場面試,感覺大廠比較考驗發散性思維,為什麼這樣做,這樣做有什麼用,會得到什麼好處,跟其他相比有什麼優勢,這確實是我之前所不具備的,思考問題一定要深入原理,多思考背後的問題,這樣才能快速成長起來。

希望能夠堅持到這裡朋友們,以後再遇到Channel的問題,不會再被難住,加油!如果友友們覺得寫的還可以,記得一鍵三連哦!

未來不是預測,而是創造。只要我們努力、積極地行動,未來就充滿著無限的可能

相關文章