前幾天面試某大廠的雲原生崗位,原本是一個輕鬆+愉快的過程,當問到第二個問題,我就發現事情的不對勁,先覆盤一下面試官有關Channel的問題,然後再逐一解決,最後進行擴充套件,這次一定要一次性通關channel!答應我,看完這篇文章,不要再被Channel吊打了!
介紹一下Channel
Channel在go中起什麼作用
Channel為什麼需要兩個佇列實現
Go為什麼要開發Channel,而別的語言為什麼沒有
Channel底層是使用鎖控制並行的,為什麼不直接使用鎖
Channel的底層原理和資料結構
Channel的讀寫流程
Channel為什麼能做到執行緒安全
操作Channel可能出現的情況
Channel有哪些常見的使用場景
Channel的讀寫操作是否是原子性的,如何實現
如何避免在Channel中出現死鎖的情況
Channel可以在多個goroutine之間傳遞什麼型別的資料
如何在Channel中使用快取區
在使用Channel時,如何保證資料的同步性和一致性
如何保證Channel的安全性
Channel的大小是否對效能有影響
Channel的記憶體模型是什麼
如何在Channel中傳遞複雜的資料型別
Channel和goroutine之間的關係是什麼
在Go語言中,Channel和鎖的使用場景有哪些區別
Channel是Go語言中的一種並行原語,用於在goroutine之間傳遞資料和同步執行。Channel實際上是一種特殊型別的資料結構,可以將其想象成一個管道,通過它可以傳送和接收資料,實現goroutine之間的通訊和同步。
Channel的特點包括:
Channel的使用方式包括:
在 Go 中,channel
是一種用於在 goroutine 之間傳遞資料的並行原語。channel
可以讓 goroutine 在傳送和接收操作之間同步,從而避免了競態條件,從而更加安全地共用記憶體。
channel
類似於一個佇列,資料可以從一個 goroutine 中傳送到 channel
,然後從另一個 goroutine 中接收。channel
可以是有緩衝的,這意味著可以在 channel
中儲存一定數量的值,而不僅僅是一個。如果 channel
是無緩衝的,則傳送和接收操作將會同步阻塞,直到有 goroutine 準備好接收或傳送資料。
注:我這裡提到了Channel底層用到了兩個佇列實現。所以就有了下面的問題
一個Channel可以被看作是一個通訊通道,用於在不同的程序之間傳遞資料。在具體的實現中,一個Channel通常需要使用兩個佇列來實現。這兩個佇列是傳送佇列和接收佇列。
傳送佇列是用來儲存將要傳送的資料的佇列。當一個程序想要通過Channel傳送資料時,它會將資料新增到傳送佇列中。傳送佇列中的資料會按照先進先出的順序被逐個傳送到接收程序。如果傳送佇列已經滿了,那麼傳送程序就需要等待,直到有足夠的空間可以儲存資料。
接收佇列是用來儲存接收程序已經準備好接收的資料的佇列。當一個程序從Channel中接收資料時,它會從接收佇列中取出資料。如果接收佇列是空的,那麼接收程序就需要等待,直到有新的資料可以接收。
使用兩個佇列實現Channel的主要原因是為了實現非同步通訊。傳送程序可以在傳送資料之後立即繼續執行其他任務,而不需要等待接收程序確認收到資料。同樣,接收程序也可以在等待資料到達的同時執行其他任務。這種非同步通訊的實現方式可以提高系統的吞吐量和響應速度。
在Go語言中,Channel是一種非常重要的並行原語。Go語言將Channel作為語言內建的原語,可能是出於以下幾個方面的考慮:
雖然其他程式語言中沒有像Go語言中的Channel這樣的內建並行原語,但是許多程式語言提供了類似於Channel的實現,比如Java的ConcurrentLinkedQueue、Python的Queue、C++的std::queue等。這些實現雖然沒有Go語言中的Channel那麼簡單易用和高效,但也能夠滿足多執行緒程式設計中的資料傳輸和同步需求。
注:我這裡提到了Channel底層是使用鎖實現。所以就有了下面的問題
雖然在Go語言中,Channel底層實現是使用鎖控制並行的,但是Channel和鎖的使用場景是不同的,具有不同的優勢和適用性。
首先,Channel比鎖更加高階和抽象。Channel可以實現多個goroutine之間的同步和資料傳遞,不需要程式設計師顯式地使用鎖來進行執行緒間的協調。Channel可以避免常見的同步問題,比如死鎖、飢餓等問題。
其次,Channel在語言層面提供了一種更高效的並行模型。在使用鎖進行並行控制時,需要程式設計師自己手動管理鎖的獲取和釋放,這增加了程式碼複雜度和錯誤的風險。而使用Channel時,可以通過goroutine的排程和Channel的阻塞機制來實現更加高效和簡單的並行控制。
此外,Channel還可以避免一些由鎖導致的效能問題,如鎖競爭、鎖粒度過大或過小等問題。Channel提供了一種更加精細的控制機制,能夠更好地平衡不同goroutine之間的並行效能。
總的來說,雖然Channel底層是使用鎖控制並行的,但是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,並將其重新加入到可執行佇列中,等待排程器的排程。
向 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 出現的場景還有:
Channel的執行緒安全主要是通過其內部的同步機制實現的。
Channel 可以理解是一個先進先出的佇列,通過管道進行通訊,傳送一個資料到Channel和從Channel接收一個資料都是原子性的。不要通過共用記憶體來通訊,而是通過通訊來共用記憶體,前者就是傳統的加鎖,後者就是Channel。設計Channel的主要目的就是在多工間傳遞資料的,本身就是安全的。
當多個goroutine通過Channel進行通訊時,Channel會保證每個操作的原子性和順序性,避免了多個goroutine同時存取共用變數導致的資料競爭問題。Channel的阻塞特性也保證了在傳送和接收操作發生時,它們會被新增到等待佇列中,直到滿足條件後才會被喚醒,從而避免了死鎖問題。
channel存在3種狀態:
操作 | 一個零值nil通道 | 一個非零值但已關閉的通道 | 一個非零值且尚未關閉的通道 |
---|---|---|---|
關閉 | 產生恐慌 | 產生恐慌 | 成功關閉 |
傳送資料 | 永久阻塞 | 產生恐慌 | 阻塞或者成功傳送 |
接收資料 | 永久阻塞 | 永不阻塞 | 阻塞或者成功接收 |
Channel的讀寫操作是原子性的,並且是由Go語言內部的同步機制來保證的。
當一個goroutine進行Channel的讀寫操作時,Go語言內部會自動進行同步,保證該操作的原子性和順序性。這種同步機制主要涉及到兩個部分:
通過這種基於鎖和等待的同步機制,Go語言保證了Channel的讀寫操作是原子性的,可以在多個goroutine之間安全地進行通訊和同步。
在Go語言中,Channel可以在多個goroutine之間傳遞任何型別的資料,包括基本資料型別、複合資料型別、結構體、自定義型別等。這些資料型別在傳遞過程中都會被封裝成對應的指標型別,並由Channel進行傳遞。
在Go語言中,我們可以使用帶緩衝的Channel來實現Channel的快取區功能。帶緩衝的Channel可以儲存一定數量的元素,而不必立即將它們交給接收方。這樣可以減少傳送和接收操作之間的同步,從而提高程式的效能。
使用帶緩衝的Channel,可以通過在Channel宣告時指定緩衝區的大小來實現。例如,宣告一個容量為10的緩衝Channel可以使用以下語句:
ch := make(chan int, 10)
在這個例子中,我們建立了一個整型緩衝Channel,其容量為10。這意味著在Channel中可以儲存10個整型元素,而不必立即將它們傳送到接收方。當Channel中的元素數量達到緩衝區容量時,再進行寫入操作時,寫入操作就會被阻塞,直到有接收方讀取了Channel中的元素。
在使用Channel時,為了保證資料的同步性和一致性,可以採用以下幾種方式:
Channel的大小對效能會產生一定的影響。Channel的大小是指Channel可以容納的元素數量,可以通過在建立Channel時指定容量大小來控制。當Channel的容量較小時,可能會導致傳送和接收操作的阻塞,從而影響程式的效能。而當Channel的容量較大時,可能會增加系統的記憶體開銷,也可能會導致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的緩衝區中取出或者等待傳送方傳送資料。
在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中傳遞複雜的資料型別,需要確保該型別是可匯出的。
在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語言並行程式設計的基礎。
在Go語言中,Channel和鎖(sync.Mutex等)都可以用於並行程式設計中的同步和共用資料,但它們的使用場景有一些區別。
Channel通常用於Goroutine之間傳遞資料,並行的Goroutine之間可以通過Channel進行同步。使用Channel可以避免鎖的問題,例如死鎖、飢餓等問題。Channel可以將資料在多個Goroutine之間進行傳遞和共用,而且在資料傳遞的過程中,不需要使用鎖來保證資料的安全性,這也是Channel比鎖更加安全和高效的原因之一。因此,當需要在不同的Goroutine之間傳遞資料時,使用Channel是比較合適的選擇。
鎖通常用於對共用資源進行保護,防止多個Goroutine同時存取和修改同一個共用資源,從而導致資料的競爭和不一致。使用鎖可以保證同一時刻只有一個Goroutine能夠存取和修改共用資源,從而保證資料的安全性和一致性。當需要對共用資源進行保護時,使用鎖是比較合適的選擇。
Channel和鎖都是Go語言中常用的並行程式設計工具,它們各自有不同的使用場景。在實際開發中,應根據具體的需求選擇合適的並行程式設計工具來實現同步和共用資料。
通過這場面試,感覺大廠比較考驗發散性思維,為什麼這樣做,這樣做有什麼用,會得到什麼好處,跟其他相比有什麼優勢,這確實是我之前所不具備的,思考問題一定要深入原理,多思考背後的問題,這樣才能快速成長起來。
希望能夠堅持到這裡朋友們,以後再遇到Channel的問題,不會再被難住,加油!如果友友們覺得寫的還可以,記得一鍵三連哦!
未來不是預測,而是創造。只要我們努力、積極地行動,未來就充滿著無限的可能