golang
-
go和php的區(qū)別
類型:go為編譯性語言;php解釋性語言
錯(cuò)誤:go的錯(cuò)誤處理機(jī)制;php本身或者框架即可糾錯(cuò)
性能:go重視并發(fā)性能 php重視開發(fā)速度
應(yīng)用:go側(cè)重于容器/高性能并發(fā)/云計(jì)算/;php側(cè)重于后臺(tái)/網(wǎng)站/系統(tǒng)
-
編譯型代碼和解釋型代碼的區(qū)別
- 編譯型和解釋型語言的不同:過程發(fā)生的時(shí)機(jī)不一樣。
- 編譯型語言的代表是C,源代碼被編譯之后生成中間文件(.o和.obj),然后用連接器和匯編器生成機(jī)器碼,也就是一系列基本操作的序列,機(jī)器碼最后被執(zhí)行生成最終動(dòng)作。
- 解釋型的語言以Ruby為例,也經(jīng)歷了這些步驟,不同的是,C語言會(huì)把那些從源代碼“變”來的基本操作序列(保存)起來,而Ruby直接將這些生成的基本操作序列(Ruby虛擬機(jī))指令丟給Ruby虛擬機(jī)執(zhí)行然后產(chǎn)生動(dòng)作了。
-
go的鎖:
一旦數(shù)據(jù)被多個(gè)線程共享,那么就很可能會(huì)產(chǎn)生爭用和沖突的情況。這種情況也被稱為競態(tài)條件(race condition),這往往會(huì)破壞共享數(shù)據(jù)的一致性。
一個(gè)互斥鎖可以被用來保護(hù)一個(gè)臨界區(qū)或者一組相關(guān)臨界區(qū)。我們可以通過它來保證,在同一時(shí)刻只有一個(gè) goroutine 處于該臨界區(qū)之內(nèi)。為了兌現(xiàn)這個(gè)保證,每當(dāng)有 goroutine 想進(jìn)入臨界區(qū)時(shí),都需要先對它進(jìn)行鎖定,并且,每個(gè) goroutine 離開臨界區(qū)時(shí),都要及時(shí)地對它進(jìn)行解鎖。
sync包中的Mutex就是與其對應(yīng)的類型,該類型的值可以被稱為互斥量或者互斥鎖?;コ怄i是開箱即用的。換句話說,一旦我們聲明了一個(gè)sync.Mutex類型的變量,就可以直接使用它了。
保證多個(gè) goroutine 并發(fā)地訪問同一個(gè)共享資源時(shí)的完全串行,這是通過保護(hù)針對此共享資源的一個(gè)臨界區(qū),或一組相關(guān)臨界區(qū)實(shí)現(xiàn)的。因此,我們可以把它看做是 goroutine 進(jìn)入相關(guān)臨界區(qū)時(shí),必須拿到的訪問令牌。
- 使用互斥鎖的注意事項(xiàng)如下:
不要重復(fù)鎖定互斥鎖;
不要忘記解鎖互斥鎖,必要時(shí)使用defer語句;
不要對尚未鎖定或者已解鎖的互斥鎖解鎖;
不要在多個(gè)函數(shù)之間直接傳遞互斥鎖。
<img src="https://static001.geekbang.org/resource/image/73/6c/73d3313640e62bb95855d40c988c2e6c.png" alt="img" style="zoom:35%;" />
-
go的interface是怎么實(shí)現(xiàn)的?
只要目標(biāo)類型方法集內(nèi)包含接口聲明的全部方法。
當(dāng)我們給一個(gè)接口變量賦值的時(shí)候,該變量的動(dòng)態(tài)類型會(huì)與它的動(dòng)態(tài)值一起被存儲(chǔ)在一個(gè)專用的數(shù)據(jù)結(jié)構(gòu)中。iface的實(shí)例會(huì)包含兩個(gè)指針,一個(gè)是指向類型信息的指針,另一個(gè)是指向動(dòng)態(tài)值的指針。這里的類型信息是由另一個(gè)專用數(shù)據(jù)結(jié)構(gòu)的實(shí)例承載的,其中包含了動(dòng)態(tài)值的類型,以及使它實(shí)現(xiàn)了接口的方法和調(diào)用它們的途徑,等等??傊?,接口變量被賦予動(dòng)態(tài)值的時(shí)候,存儲(chǔ)的是包含了這個(gè)動(dòng)態(tài)值的副本的一個(gè)結(jié)構(gòu)更加復(fù)雜的值。
一個(gè)簡單的邏輯就是需要獲取這個(gè)類型的所有方法集合(集合A),并獲取該接口包含的所有方法集合(集合B),然后判斷列表B是否為列表A的子集。
-
golang的并發(fā)模式
goroutine是go的輕量級線程實(shí)現(xiàn)。
CSP (Communicating Sequential Process,通訊順序進(jìn)程) 模型不同于傳統(tǒng)的多線程通過共享內(nèi)存來通信,CSP講究的是“以通信的方式來共享內(nèi)存”。用于描述兩個(gè)獨(dú)立的并發(fā)實(shí)體通過共享的通訊 channel(管道)進(jìn)行通信的并發(fā)模型。 CSP中channel是第一類對象,它不關(guān)注發(fā)送消息的實(shí)體,而關(guān)注與發(fā)送消息時(shí)使用的channel。
-
通過channel通知實(shí)現(xiàn)并發(fā)控制
func main() { ch := make(chan struct{}) go func() { fmt.Println("do something..") time.Sleep(time.Second * 1) ch <- struct{}{} }() <-ch fmt.Println("I am finished") } -
通過sync包中的WaitGroup實(shí)現(xiàn)并發(fā)控制
在
sync包中,提供了WaitGroup,它會(huì)等待它收集的所有goroutine任務(wù)全部完成。在WaitGroup里主要有三個(gè)方法- Add, 可以添加或減少 goroutine的數(shù)量
- Done, 相當(dāng)于Add(-1)
- Wait, 執(zhí)行后會(huì)堵塞主線程,直到WaitGroup 里的值減至0
func main(){ var wg sync.WaitGroup var urls = []string{ "http://www.golang.org/", "http://www.google.com/", "http://www.somestupidname.com/", } for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() http.Get(url) }(url) } wg.Wait() } -
Context。
goroutine的上下文。它是包括一個(gè)程序的運(yùn)行環(huán)境、現(xiàn)場和快照等。每個(gè)程序要運(yùn)行時(shí),都需要知道當(dāng)前程序的運(yùn)行狀態(tài),通常Go 將這些封裝在一個(gè)Context里,再將它傳給要執(zhí)行的goroutine。在每一個(gè)循環(huán)中產(chǎn)生一個(gè)
goroutine,每一個(gè)goroutine中都傳入context,在每個(gè)goroutine中通過傳入ctx創(chuàng)建一個(gè)子Context,并且通過select一直監(jiān)控該Context的運(yùn)行情況,當(dāng)在父Context退出的時(shí)候,代碼中并沒有明顯調(diào)用子Context的Cancel函數(shù),但是分析結(jié)果,子Context還是被正確合理的關(guān)閉了,這是因?yàn)椋谢谶@個(gè)Context或者衍生的子Context都會(huì)收到通知,這時(shí)就可以進(jìn)行清理操作了,最終釋放goroutine,這就優(yōu)雅的解決了goroutine啟動(dòng)后不可控的問題。package main import ( "context" "fmt" "sync" "time" ) type Message struct { netId int Data string } type ServerConn struct { sendCh chan Message handleCh chan Message wg *sync.WaitGroup ctx context.Context cancel context.CancelFunc netId int } func main() { conn := &ServerConn{ sendCh: make(chan Message), handleCh: make(chan Message), wg: &sync.WaitGroup{}, netId: 100, } conn.ctx, conn.cancel = context.WithCancel(context.WithValue(context.Background(), "key", conn.netId)) loopers := []func(*ServerConn, *sync.WaitGroup){readLoop, writeLoop, handleLoop} for _, looper := range loopers { conn.wg.Add(1) go looper(conn, conn.wg) } go func() { time.Sleep(time.Second * 3) conn.cancel() }() conn.wg.Wait() } func readLoop(c *ServerConn, wg *sync.WaitGroup) { netId, _ := c.ctx.Value("key").(int) handlerCh := c.handleCh ctx, _ := context.WithCancel(c.ctx) cDone := ctx.Done() defer wg.Done() for { time.Sleep(time.Second * 1) select { case <-cDone: fmt.Println("readLoop close") return default: handlerCh <- Message{netId, "Hello world"} } } } func handleLoop(c *ServerConn, wg *sync.WaitGroup) { handlerCh := c.handleCh sendCh := c.sendCh ctx, _ := context.WithCancel(c.ctx) cDone := ctx.Done() defer wg.Done() for { select { case handleData, ok := <-handlerCh: if ok { handleData.netId++ handleData.Data = "I am whole world" sendCh <- handleData } case <-cDone: fmt.Println("handleLoop close") return } } } func writeLoop(c *ServerConn, wg *sync.WaitGroup) { sendCh := c.sendCh ctx, _ := context.WithCancel(c.ctx) cDone := ctx.Done() defer wg.Done() for { select { case sendData, ok := <-sendCh: if ok { fmt.Println(sendData) } case <-cDone: fmt.Println("writeLoop close") return } } }
-
-
goroutine、channel。
Goroutine是Go中最基本的執(zhí)行單元。事實(shí)上每一個(gè)Go程序至少有一個(gè)goroutine:主goroutine。當(dāng)程序啟動(dòng)時(shí),它會(huì)自動(dòng)創(chuàng)建。goroutine是Go語言的基本調(diào)度單位,而channel則是它們之間的通信機(jī)制。操作符<-用來指定管道的方向,發(fā)送或接收。
Go 語言里的并發(fā)指的是能讓某個(gè)函數(shù)獨(dú)立于其他函數(shù)運(yùn)行的能力。當(dāng)一個(gè)函數(shù)創(chuàng)建為 goroutine 時(shí),Go 會(huì)將其視為一個(gè)獨(dú)立的工作單元。這個(gè)單元會(huì)被調(diào)度到可用的邏輯處理器上執(zhí)行。Go 語言 運(yùn)行時(shí)的調(diào)度器是一個(gè)復(fù)雜的軟件,能管理被創(chuàng)建的所有 goroutine 并為其分配執(zhí)行時(shí)間。這個(gè)調(diào)度 器在操作系統(tǒng)之上,將操作系統(tǒng)的線程與語言運(yùn)行時(shí)的邏輯處理器綁定,并在邏輯處理器上運(yùn)行 goroutine。調(diào)度器在任何給定的時(shí)間,都會(huì)全面控制哪個(gè) goroutine 要在哪個(gè)邏輯處理器上運(yùn)行。
Go 語言的并發(fā)同步模型來自一個(gè)叫作通信順序進(jìn)程(Communicating Sequential Processes,CSP) 的范型(paradigm)。CSP 是一種消息傳遞模型,通過在 goroutine 之間傳遞數(shù)據(jù)來傳遞消息,而不是 對數(shù)據(jù)進(jìn)行加鎖來實(shí)現(xiàn)同步訪問。用于在 goroutine 之間同步和傳遞數(shù)據(jù)的關(guān)鍵數(shù)據(jù)類型叫作通道(channel)。對于沒有使用過通道寫并發(fā)程序的程序員來說,通道會(huì)讓他們感覺神奇而興奮。希望讀 者使用后也能有這種感覺。使用通道可以使編寫并發(fā)程序更容易,也能夠讓并發(fā)程序出錯(cuò)更少。
-
線程和協(xié)程
線程:
當(dāng)運(yùn)行一個(gè)應(yīng)用程序的時(shí)候,操作系統(tǒng)會(huì)給這個(gè)應(yīng)用程序啟動(dòng)一個(gè)進(jìn)程。我們可以將進(jìn)程看作一個(gè)包含應(yīng)用程序在運(yùn)行中需要用到和維護(hù)的各種資源的容器。一個(gè)進(jìn)程至少包含一個(gè)線程,這個(gè)線程就是主線程。操作系統(tǒng)會(huì)調(diào)度線程到不同的CPU上執(zhí)行,這個(gè)CPU不一定就是進(jìn)程所在的CPU。
<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200402232450525.png" alt="image-20200402232450525" style="zoom:40%;" />
- 進(jìn)程:資源的所有權(quán)
- 線程:執(zhí)行和調(diào)度的基本單位
- 同一進(jìn)程下的各個(gè)線程共享資源,但寄存器、棧、PC不共享
協(xié)程:也有人稱之為輕量級線程,具備以下幾個(gè)特點(diǎn):
- 能夠在單一的系統(tǒng)線程中模擬多個(gè)任務(wù)的并發(fā)執(zhí)行。
- 在一個(gè)特定的時(shí)間,只有一個(gè)任務(wù)在運(yùn)行,即并非真正地并行。
- 被動(dòng)的任務(wù)調(diào)度方式,即任務(wù)沒有主動(dòng)搶占時(shí)間片的說法。當(dāng)一個(gè)任務(wù)正在執(zhí)行時(shí),外部沒有辦法中止它。要進(jìn)行任務(wù)切換,只能通過由該任務(wù)自身調(diào)用yield()來主動(dòng)出讓 CPU使用權(quán)。
- 每個(gè)協(xié)程都有自己的堆棧和局部變量。
每個(gè)協(xié)程都包含3種運(yùn)行狀態(tài):掛起、運(yùn)行和停止。停止通常表示該協(xié)程已經(jīng)執(zhí)行完成。每個(gè)協(xié)程都包含3種運(yùn)行狀態(tài):掛起、運(yùn)行和停止。停止通常表示該協(xié)程已經(jīng)執(zhí)行完成(包括遇到問題明確執(zhí)行退出),掛起則表示該協(xié)程尚未執(zhí)行完成,但出讓了時(shí)間片,以后 有機(jī)會(huì)時(shí)會(huì)由調(diào)度器繼續(xù)執(zhí)行。
線程與協(xié)程的區(qū)別:
- 可控的切換時(shí)機(jī),一旦創(chuàng)建完線程,你就無法決定他什么時(shí)候獲得時(shí)間片,什么時(shí)候讓出時(shí)間片了,你把它交給了內(nèi)核。而協(xié)程可以有。
- 很小的切換代價(jià),從操作系統(tǒng)有沒有調(diào)度權(quán)上看,協(xié)程就是因?yàn)椴恍枰M(jìn)行內(nèi)核態(tài)的切換,所以會(huì)使用它,會(huì)有這么個(gè)東西。
-
Golang 的協(xié)程信通訊方式有哪些。
- 全局共享變量
- channel通信
- Context包
-
垃圾回收機(jī)制
- 三色標(biāo)記法是對標(biāo)記階段的改進(jìn)
- 初始狀態(tài)所有對象都是白色。
- 從root根出發(fā)掃描所有根對象(下圖a,b),將他們引用的對象標(biāo)記為灰色(圖中A,B)。root區(qū)域主要是程序運(yùn)行到當(dāng)前時(shí)刻的棧和全局?jǐn)?shù)據(jù)區(qū)域。
- 分析灰色對象是否引用了其他對象。如果沒有引用其它對象則將該灰色對象標(biāo)記為黑色(上圖中A);
- 如果有引用則將它變?yōu)楹谏耐瑫r(shí)將它引用的對象也變?yōu)榛疑ㄉ蠄D中B引用了D)
重復(fù)步驟3,直到灰色對象隊(duì)列為空。此時(shí)白色對象即為垃圾,進(jìn)行回收。
- 三色標(biāo)記法是對標(biāo)記階段的改進(jìn)
<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403000810827.png" alt="image-20200403000810827" style="zoom:50%;" /><img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403000829012.png" alt="image-20200403000829012" style="zoom:50%;" />
<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403000857955.png" alt="image-20200403000857955" style="zoom:50%;" /><img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403000921980.png" alt="image-20200403000921980" style="zoom:50%;" />
-
GC流程
其實(shí)是因?yàn)镚olang GC的大部分處理是和用戶代碼并行的。GC期間用戶代碼可能會(huì)改變某些對象的狀態(tài),如何實(shí)現(xiàn)GC和用戶代碼并行呢?先看下GC工作的完整流程:
Mark: 包含兩部分:
Mark Prepare: 初始化GC任務(wù),包括開啟寫屏障(write barrier)和輔助GC(mutator assist),統(tǒng)計(jì)root對象的任務(wù)數(shù)量等。這個(gè)過程需要STW
GC Drains: 掃描所有root對象,包括全局指針和goroutine(G)棧上的指針(掃描對應(yīng)G棧時(shí)需停止該G),將其加入標(biāo)記隊(duì)列(灰色隊(duì)列),并循環(huán)處理灰色隊(duì)列的對象,直到灰色隊(duì)列為空。該過程后臺(tái)并行執(zhí)行
Mark Termination: 完成標(biāo)記工作,重新掃描(re-scan)全局指針和棧。因?yàn)镸ark和用戶程序是并行的,所以在Mark過程中可能會(huì)有新的對象分配和指針賦值,這個(gè)時(shí)候就需要通過寫屏障(write barrier)記錄下來,re-scan 再檢查一下。這個(gè)過程也是會(huì)STW的。
Sweep: 按照標(biāo)記結(jié)果回收所有的白色對象,該過程后臺(tái)并行執(zhí)行
Sweep Termination: 對未清掃的span進(jìn)行清掃, 只有上一輪的GC的清掃工作完成才可以開始新一輪的GC。
如果標(biāo)記期間用戶邏輯改變了剛打完標(biāo)記的對象的引用狀態(tài),怎么辦呢。
就是在每一輪GC開始時(shí)會(huì)初始化一個(gè)叫做“屏障”的東西,然后由它記錄第一次scan時(shí)各個(gè)對象的狀態(tài),以便和第二次re-scan進(jìn)行比對,引用狀態(tài)變化的對象被標(biāo)記為灰色以防止丟失,將屏障前后狀態(tài)未變化對象繼續(xù)處理。
-
GC 觸發(fā)時(shí)機(jī)
- 超過內(nèi)存大小閾值
- 達(dá)到定時(shí)時(shí)間 閾值是由一個(gè)gcpercent的變量控制的,當(dāng)新分配的內(nèi)存占已在使用中的內(nèi)存的比例超過gcprecent時(shí)就會(huì)觸發(fā)。
-
GPM并發(fā)調(diào)度
<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403003427220.png" alt="image-20200403003427220" style="zoom:40%;" />
首先是 Processor(簡稱 P),其作用類似于 CPU 核,用來控制可同時(shí)并發(fā)執(zhí)行的任務(wù)數(shù)量。每個(gè)工作線程都必須綁定一個(gè)有效 P 才被允許執(zhí)行任務(wù),否則只能休眠,直到有空閑 P 時(shí)被喚醒。P 還為線程提供執(zhí)行資源,比如對象分配內(nèi)存、本地任務(wù)隊(duì)列等。線程獨(dú)享所綁定的P資源,可在無鎖狀態(tài)下執(zhí)行高效操作。
基本上,進(jìn)程內(nèi)的一切都在以 goroutine(簡稱 G)方式運(yùn)行,包括運(yùn)行時(shí)相關(guān)服務(wù),以及 main.main 入口函數(shù)。需要指出,G并非執(zhí)行體,它僅僅保存并發(fā)任務(wù)狀態(tài),為任務(wù)執(zhí)行提供所需棧內(nèi)存空間。G 任務(wù)創(chuàng)建后被放置在P本地隊(duì)列或全局隊(duì)列,等待工作線程調(diào)度執(zhí)行。
實(shí)際執(zhí)行體是系統(tǒng)線程(簡稱 M),它和P綁定,以調(diào)度循環(huán)方式不停執(zhí)行G 并發(fā)任務(wù)。M通過修改寄存器,將執(zhí)行棧指向G自帶的棧內(nèi)存,并在此空間內(nèi)分配堆棧幀,執(zhí)行任務(wù)函數(shù)。當(dāng)需要中途切換時(shí),只要將相關(guān)寄存器值保存回G空間即可維護(hù)狀態(tài),任務(wù)M都可據(jù)此恢復(fù)執(zhí)行。線程僅負(fù)責(zé)執(zhí)行,不再持有狀態(tài),這是并發(fā)任務(wù)跨線程調(diào)度,實(shí)現(xiàn)多路復(fù)用的根本所在。
盡管 P/M 構(gòu)成執(zhí)行組合體,但兩者數(shù)量并非一一對應(yīng)。通常情況下,P 的數(shù)量相對恒定,默認(rèn)與CPU核數(shù)量相同,但也可能更多或更少,而M則是由調(diào)度器按需創(chuàng)建的。舉例來說,當(dāng)M 因陷入系統(tǒng)調(diào)用而長時(shí)間阻塞時(shí),P 就會(huì)被監(jiān)控線程搶回,去新建(或喚醒)一個(gè)M執(zhí)行其他任務(wù),這樣M的數(shù)量就會(huì)增長。
因?yàn)镚初始棧僅有 2KB,且創(chuàng)建操作只是在用戶空間簡單地分配對象,遠(yuǎn)比進(jìn)入內(nèi)核態(tài)分配線程要簡單得多。調(diào)度器讓多個(gè)M進(jìn)入調(diào)度循環(huán),不停獲取并執(zhí)行任務(wù),所以我們才能創(chuàng)建成千上萬個(gè)并發(fā)任務(wù)。
操作系統(tǒng)線程、邏輯處理器和本地運(yùn)行隊(duì)列之間的關(guān)系。如果創(chuàng)建一個(gè) goroutine 并準(zhǔn)備運(yùn)行,這個(gè) goroutine 就會(huì)被放到調(diào)度器的全局運(yùn)行隊(duì)列中。之后,調(diào)度器就將這些隊(duì)列中的 goroutine 分配給一個(gè)邏輯處理器,并放到這個(gè)邏輯處理器對應(yīng)的本地運(yùn)行隊(duì)列中。本地運(yùn)行隊(duì)列中的 goroutine 會(huì)一直等待直到自己被分配的邏輯處理器執(zhí)行。
<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403003703442.png" alt="image-20200403003703442" style="zoom:40%;" />
-
Golang 里的逃逸分析是什么?怎么避免內(nèi)存逃逸?
- 定義
在編譯程序優(yōu)化理論中,逃逸分析是一種確定指針動(dòng)態(tài)范圍的方法,簡單來說就是分析在程序的哪些地方可以訪問到該指針。
再往簡單的說,Go是通過在編譯器里做逃逸分析(escape analysis)來決定一個(gè)對象放棧上還是放堆上,不逃逸的對象放棧上,可能逃逸的放堆上;即我發(fā)現(xiàn)變量在退出函數(shù)后沒有用了,那么就把丟到棧上,畢竟棧上的內(nèi)存分配和回收比堆上快很多;反之,函數(shù)內(nèi)的普通變量經(jīng)過逃逸分析后,發(fā)現(xiàn)在函數(shù)退出后變量還有在其他地方上引用,那就將變量分配在堆上。做到按需分配。
- 存在目的
堆(Heap):一般來講是人為手動(dòng)進(jìn)行管理,手動(dòng)申請、分配、釋放。堆適合不可預(yù)知大小的內(nèi)存分配,這也意味著為此付出的代價(jià)是分配速度較慢,而且會(huì)形成內(nèi)存碎片。
棧(Stack):由編譯器進(jìn)行管理,自動(dòng)申請、分配、釋放。一般不會(huì)太大,因此棧的分配和回收速度非???;我們常見的函數(shù)參數(shù)(不同平臺(tái)允許存放的數(shù)量不同),局部變量等都會(huì)存放在棧上。
棧分配內(nèi)存只需要兩個(gè)CPU指令:“PUSH”和“RELEASE”,分配和釋放;而堆分配內(nèi)存首先需要去找到一塊大小合適的內(nèi)存塊,之后要通過垃圾回收才能釋放。
通俗比喻的說,棧就如我們?nèi)ワ堭^吃飯,只需要點(diǎn)菜(發(fā)出申請)--》吃吃吃(使用內(nèi)存)--》吃飽就跑剩下的交給飯館(操作系統(tǒng)自動(dòng)回收),而堆就如在家里做飯,大到家,小到買什么菜,每一個(gè)環(huán)節(jié)都需要自己來實(shí)現(xiàn),但是自由度會(huì)大很多。
我們就可以更好的知道逃逸分析存在的目的了:
? 1. 減少gc壓力,棧上的變量,隨著函數(shù)退出后系統(tǒng)直接回收,不需要gc標(biāo)記后再清除。
? 2. 減少內(nèi)存碎片的產(chǎn)生。
? 3. 減輕分配堆內(nèi)存的開銷,提高程序的運(yùn)行速度。
- 避免內(nèi)存逃逸
不要盲目使用變量的指針作為函數(shù)參數(shù),雖然它會(huì)減少復(fù)制操作。但其實(shí)當(dāng)參數(shù)為變量自身的時(shí)候,復(fù)制是在棧上完成的操作,開銷遠(yuǎn)比變量逃逸后動(dòng)態(tài)地在堆上分配內(nèi)存少的多。
-
slice底層原理
切片是一個(gè)很小的對象,對底層數(shù)組進(jìn)行了抽象,并提供相關(guān)的操作方法。切片有 3 個(gè)字段 的數(shù)據(jù)結(jié)構(gòu),這些數(shù)據(jù)結(jié)構(gòu)包含 Go 語言需要操作底層數(shù)組的元數(shù)據(jù)。這 3 個(gè)字段分別是指向底層數(shù)組的指針、切片訪問的元素的個(gè)數(shù)(即長度)和切片允許增長 到的元素個(gè)數(shù)(即容量)。后面會(huì)進(jìn)一步講解長度和容量的區(qū)別。
<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403005146656.png" alt="image-20200403005146656" style="zoom:40%;" />
-
Golang 的默認(rèn)參數(shù)傳遞方式以及哪些是引用傳遞?
默認(rèn)采用值傳遞,且Go 中函數(shù)傳參僅有值傳遞一種方式。傳參方式。slice、map、channel 都是引用類型。
for 和 for range 區(qū)別:for range 是值拷貝,隨機(jī)遍歷。
-
golang的特點(diǎn)
- 開發(fā)速度:使用了更加智能的編譯器,并簡化了解決依賴的算法,最終提供了更快的編譯速度。
- 并發(fā):在 goroutine 之間發(fā)送消息,而不是讓多個(gè) goroutine 爭奪同一個(gè)數(shù)據(jù)的使用權(quán)。
- 類型系統(tǒng):提供了靈活的、無繼承的類型系統(tǒng),無需降低運(yùn)行性能就能最大程度上復(fù)用代碼。還具有獨(dú)特的接口實(shí)現(xiàn)機(jī)制,允許用戶對行為進(jìn)行建模,而不是對類型進(jìn)行建模。
- 內(nèi)存管理:使用內(nèi)存前要先分配這段內(nèi)存,而且使用完畢后要將其釋放掉。
Go 如何靜態(tài)的去看線程是否安全:可以使用 go run -race 或者 go build -race來進(jìn)行靜態(tài)檢測。
-
用戶態(tài)和內(nèi)核態(tài)
內(nèi)核態(tài):控制計(jì)算機(jī)的硬件資源,并提供上層應(yīng)用程序運(yùn)行的環(huán)境。
用戶態(tài):上層應(yīng)用程序的活動(dòng)空間,應(yīng)用程序的執(zhí)行必須依托于內(nèi)核提供的資源。
-
用戶態(tài)切換為內(nèi)核態(tài)的三種情況:
- 系統(tǒng)調(diào)用:為了使上層應(yīng)用能夠訪問到這些資源,內(nèi)核為上層應(yīng)用提供訪問的接口。
- 異常事件: 當(dāng)CPU正在執(zhí)行運(yùn)行在用戶態(tài)的程序時(shí),突然發(fā)生某些預(yù)先不可知的異常事件,這個(gè)時(shí)候就會(huì)觸發(fā)從當(dāng)前用戶態(tài)執(zhí)行的進(jìn)程轉(zhuǎn)向內(nèi)核態(tài)執(zhí)行相關(guān)的異常事件,典型的如缺頁異常。
- 外圍設(shè)備的中斷:當(dāng)外圍設(shè)備完成用戶的請求操作后,會(huì)像CPU發(fā)出中斷信號(hào),此時(shí),CPU就會(huì)暫停執(zhí)行下一條即將要執(zhí)行的指令,轉(zhuǎn)而去執(zhí)行中斷信號(hào)對應(yīng)的處理程序,如果先前執(zhí)行的指令是在用戶態(tài)下,則自然就發(fā)生從用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)換。
channel緩沖的問題:帶有緩沖(buffer)channel則可以非阻塞容納N個(gè)元素。發(fā)送數(shù)據(jù)到緩沖(buffer) channel不會(huì)被阻塞,除非channel已滿;同樣的,從緩沖(buffer) channel取數(shù)據(jù)也不會(huì)被阻塞,除非channel空了。
-
golang的select有多個(gè)case時(shí)如何選擇執(zhí)行順序。
- select機(jī)制簡述
- select+case是用于阻塞監(jiān)聽goroutine的,如果沒有case,就單單一個(gè)select{},則為監(jiān)聽當(dāng)前程序中的goroutine,此時(shí)注意,需要有真實(shí)的goroutine在跑,否則select{}會(huì)報(bào)panic
- select底下有多個(gè)可執(zhí)行的case,則隨機(jī)執(zhí)行一個(gè)。
- select常配合for循環(huán)來監(jiān)聽channel有沒有故事發(fā)生。需要注意的是在這個(gè)場景下,break只是退出當(dāng)前select而不會(huì)退出for,需要用break TIP / goto的方式。
- 無緩沖的通道,則傳值后立馬close,則會(huì)在close之前阻塞,有緩沖的通道則即使close了也會(huì)繼續(xù)讓接收后面的值
- 同個(gè)通道多個(gè)goroutine進(jìn)行關(guān)閉,可用recover panic的方式來判斷通道關(guān)閉問題
看完以上知識(shí)點(diǎn)其實(shí)還是沒法解釋本文的核心疑惑,繼續(xù)往下!
- select機(jī)制簡述
go select思想來源于網(wǎng)絡(luò)IO模型中的select,本質(zhì)上也是IO多路復(fù)用,只不過這里的IO是基于channel而不是基于網(wǎng)絡(luò),同時(shí)go select也有一些自己不同的特性:
1. 每個(gè)case都必須是一個(gè)通信
2. 所有channel表達(dá)式都會(huì)被求值
3. 所有被發(fā)送的表達(dá)式都會(huì)被求值
4. 如果任意某個(gè)通信可以進(jìn)行,它就執(zhí)行;其他被忽略。
5. 如果有多個(gè)case都可以運(yùn)行,select會(huì)隨機(jī)公平地選出一個(gè)執(zhí)行。其他不會(huì)執(zhí)行。否則執(zhí)行default子句(如果有)
6. 如果沒有default字句,select將阻塞,直到某個(gè)通信可以運(yùn)行;Go不會(huì)重新對channel或值進(jìn)行求值。
-
golang的變量分配
-
Data segment:
- 初始化的全局變量或靜態(tài)變量,會(huì)被分配在 Data 段。
- 未初始化的全局變量或靜態(tài)變量,會(huì)被分配在 BSS 段。
- 在函數(shù)中定義的局部變量,會(huì)被分配在堆(Heap 段)或棧(Stack 段)。
- 實(shí)際上,如果考慮到 編譯器優(yōu)化,局部變量還可能會(huì)被 分配在寄存器,或者直接被 優(yōu)化去掉。
-
對于 Go 而言,有兩個(gè)地方可以用于分配:
堆(heap):由 GC 負(fù)責(zé)回收。對應(yīng)于進(jìn)程地址空間的堆。
棧(stack):不涉及 GC 操作。每個(gè) goroutine 都有自己的棧,初始時(shí)被分配在進(jìn)程地址空間的棧上,擴(kuò)容時(shí)被分配在進(jìn)程地址空間的堆上。
-
Go 變量主要分為兩種:
全局變量:會(huì)被 Go 編譯器標(biāo)記為一些特殊的 符號(hào)類型,分配在堆上還是棧上目前尚不清楚,不過不是本文討論的重點(diǎn)。
局部變量:對于在函數(shù)中定義的 Go 局部變量:要么被分配在堆上,要么被分配在棧上。
-
- Go 編譯器會(huì)盡可能將變量分配在棧上,以下兩種情況,Go 編譯器會(huì)將變量分配在堆上:
- 如果一個(gè)變量被取地址(has its address taken),并且被逃逸分析(escape analysis)識(shí)別為 “逃逸到堆”(escapes to heap)
- 如果一個(gè)變量很大(very large)
- 結(jié)論:
- make([]struct{}, n) 只會(huì)被分配在棧上,而不會(huì)被分配在堆上。
- Brad Fitzpatrick 的注釋是對的,并且他的意思是 “不會(huì)引發(fā)堆分配”。
- 逃逸分析識(shí)別出 escapes to heap,并不一定就是堆分配,也可能是棧分配。
- 進(jìn)行內(nèi)存分配器追蹤時(shí),如果采集不到堆分配信息,那一定只有棧分配。
- 如何確定一個(gè) Go 變量會(huì)被分配在哪里?
- 先對代碼作逃逸分析。
- 如果該變量被識(shí)別為 escapes to heap,那么它十有八九是被分配在堆上。
- 如果該變量被識(shí)別為 does not escape,或者沒有與之相關(guān)的分析結(jié)果,那么它一定是被分配在棧上。
- 如果對 escapes to heap 心存疑惑,就對代碼作內(nèi)存分配器追蹤。
- 如果有采集到與該變量相關(guān)的分配信息,那么它一定是被分配在堆上。否則,該變量一定是被分配在棧上。
-
怎么友好的關(guān)閉chan。
只由 sender 來關(guān)閉
一般不考慮關(guān)閉,除了一種情況:receiver 必須知道 sender 已經(jīng)停止發(fā)送了
如果有多個(gè)發(fā)送者,就用一個(gè) sync.WaitGroup,每次增加發(fā)送者時(shí) Add,發(fā)送者結(jié)束時(shí) Done,最后在需要關(guān)閉的時(shí)候 Wait 完再 close。通知發(fā)送者結(jié)束可以用 context.Context.Done。
-
生產(chǎn)者消費(fèi)型,用channel通信。
package main import ( "fmt" ) var c = make(chan int, 50) var count = 0 func main() { for i := 0; i < 5; i++ { go consumer(i) } for i := 0; i < 1000; i++ { c <- i } /** here **/ fmt.Println(count) } func consumer(index int) { for target := range c { fmt.Printf("no.%d:%d\n", index, target) count++ } }package main import ( "fmt" "sync" ) var c = make(chan int, 50) var count = 0 var wg = new(sync.WaitGroup) func main() { for i := 0; i < 5; i++ { wg.Add(1) go consumer(i) } for i := 0; i < 1000; i++ { c <- i } wg.Wait() close(c) /** here **/ fmt.Println(count) } func consumer(index int) { for target := range c { fmt.Printf("no.%d:%d\n", index, target) count++ if len(c) <= 0 { wg.Done() } } } -
go實(shí)現(xiàn)協(xié)程池,用channel。
package main import "fmt" import "time" //***************************Task部分 //Task對外公開,所以要export type Task struct { f func() error //一個(gè)task代表一個(gè)任務(wù),函數(shù)形式 } func NewTask(arg_f func() error) *Task { return &Task{ f:arg_f, } } func (t *Task) Execute() { t.f() } //***************************Pool部分 type Pool struct { EntryChan chan *Task //對外的任務(wù)入口 JobsChan chan *Task //內(nèi)部的任務(wù)隊(duì)列 workerNum int //協(xié)程池最大的worker數(shù) } func NewPool(num int) *Pool { return &Pool{ EntryChan:make(chan *Task), JobsChan:make(chan *Task), workerNum:num, } } //Pool創(chuàng)建worker,由worker一直去JobsChan取任務(wù)并執(zhí)行 func (p *Pool) worker(workerId int) { for task := range p.JobsChan { task.Execute() fmt.Println("workerID ", workerId, " 執(zhí)行完了一個(gè)任務(wù)") } } //Pool運(yùn)行 func (p *Pool) run() { //根據(jù)workerNum創(chuàng)建workers for i:=0;i<p.workerNum;i++ { go p.worker(i) //i作為workerId } //從EntryChan取任務(wù),發(fā)送給JobsChan for task := range p.EntryChan { p.JobsChan <- task } } func main() { //創(chuàng)建任務(wù) t := NewTask(func() error { fmt.Println(time.Now()) }) //創(chuàng)建協(xié)程池 p := NewPool(4) //把任務(wù)交給協(xié)程池 go func() { for { p.EntryChan <- t } } //啟動(dòng)協(xié)程池 p.run() }