Golang面試問題匯總
通常我們去面試肯定會有些不錯的Golang的面試題目的,所以總結下,讓其他Golang開發者也可以查看到,同時也用來檢測自己的能力和提醒自己的不足之處,歡迎大家補充和提交新的面試題目.
Golang面試問題匯總:
1. Golang中除了加Mutex鎖以外還有哪些方式安全讀寫共享變量?
Golang中Goroutine 可以通過 Channel 進行安全讀寫共享變量。
2. 無緩沖 Chan 的發送和接收是否同步?
ch := make(chan int) 無緩沖的channel由於沒有緩沖發送和接收需要同步. ch := make(chan int, 2) 有緩沖channel不要求發送和接收操作同步.
- channel無緩沖時,發送阻塞直到數據被接收,接收阻塞直到讀到數據。
- channel有緩沖時,當緩沖滿時發送阻塞,當緩沖空時接收阻塞。
3. go語言的並發機制以及它所使用的CSP並發模型.
CSP模型是上個世紀七十年代提出的,不同於傳統的多線程通過共享內存來通信,CSP講究的是“以通信的方式來共享內存”。用於描述兩個獨立的並發實體通過共享的通訊 channel(管道)進行通信的並發模型。 CSP中channel是第一類對象,它不關注發送消息的實體,而關注與發送消息時使用的channel。
Golang中channel 是被單獨創建並且可以在進程之間傳遞,它的通信模式類似於 boss-worker 模式的,一個實體通過將消息發送到channel 中,然后又監聽這個 channel 的實體處理,兩個實體之間是匿名的,這個就實現實體中間的解耦,其中 channel 是同步的一個消息被發送到 channel 中,最終是一定要被另外的實體消費掉的,在實現原理上其實類似一個阻塞的消息隊列。
Goroutine 是Golang實際並發執行的實體,它底層是使用協程(coroutine)實現並發,coroutine是一種運行在用戶態的用戶線程,類似於 greenthread,go底層選擇使用coroutine的出發點是因為,它具有以下特點:
- 用戶空間 避免了內核態和用戶態的切換導致的成本。
- 可以由語言和框架層進行調度。
- 更小的棧空間允許創建大量的實例。
Golang中的Goroutine的特性:
Golang內部有三個對象: P對象(processor) 代表上下文(或者可以認為是cpu),M(work thread)代表工作線程,G對象(goroutine).
正常情況下一個cpu對象啟一個工作線程對象,線程去檢查並執行goroutine對象。碰到goroutine對象阻塞的時候,會啟動一個新的工作線程,以充分利用cpu資源。 所有有時候線程對象會比處理器對象多很多.
我們用如下圖分別表示P、M、G:
G(Goroutine) :我們所說的協程,為用戶級的輕量級線程,每個Goroutine對象中的sched保存着其上下文信息.
M(Machine) :對內核級線程的封裝,數量對應真實的CPU數(真正干活的對象).
P(Processor) :即為G和M的調度對象,用來調度G和M之間的關聯關系,其數量可通過GOMAXPROCS()來設置,默認為核心數.
在單核情況下,所有Goroutine運行在同一個線程(M0)中,每一個線程維護一個上下文(P),任何時刻,一個上下文中只有一個Goroutine,其他Goroutine在runqueue中等待。
一個Goroutine運行完自己的時間片后,讓出上下文,自己回到runqueue中(如下圖所示)。
當正在運行的G0阻塞的時候(可以需要IO),會再創建一個線程(M1),P轉到新的線程中去運行。
當M0返回時,它會嘗試從其他線程中“偷”一個上下文過來,如果沒有偷到,會把Goroutine放到Global runqueue中去,然后把自己放入線程緩存中。 上下文會定時檢查Global runqueue。
Golang是為並發而生的語言,Go語言是為數不多的在語言層面實現並發的語言;也正是Go語言的並發特性,吸引了全球無數的開發者。
Golang的CSP並發模型,是通過Goroutine和Channel來實現的。
Goroutine 是Go語言中並發的執行單位。有點抽象,其實就是和傳統概念上的”線程“類似,可以理解為”線程“。 Channel是Go語言中各個並發結構體(Goroutine)之前的通信機制。通常Channel,是各個Goroutine之間通信的”管道“,有點類似於Linux中的管道。
通信機制channel也很方便,傳數據用channel <- data,取數據用<-channel。
在通信過程中,傳數據channel <- data和取數據<-channel必然會成對出現,因為這邊傳,那邊取,兩個goroutine之間才會實現通信。
而且不管傳還是取,必阻塞,直到另外的goroutine傳或者取為止。
4. Golang 中常用的並發模型?
Golang 中常用的並發模型有三種:
- 通過channel通知實現並發控制
無緩沖的通道指的是通道的大小為0,也就是說,這種類型的通道在接收前沒有能力保存任何值,它要求發送 goroutine 和接收 goroutine 同時准備好,才可以完成發送和接收操作。
從上面無緩沖的通道定義來看,發送 goroutine 和接收 gouroutine 必須是同步的,同時准備后,如果沒有同時准備好的話,先執行的操作就會阻塞等待,直到另一個相對應的操作准備好為止。這種無緩沖的通道我們也稱之為同步通道。
func main() { ch := make(chan struct{}) go func() { fmt.Println("start working") time.Sleep(time.Second * 1) ch <- struct{}{} }() <-ch fmt.Println("finished") }
當主 goroutine 運行到 <-ch 接受 channel 的值的時候,如果該 channel 中沒有數據,就會一直阻塞等待,直到有值。 這樣就可以簡單實現並發控制
- 通過sync包中的WaitGroup實現並發控制
Goroutine是異步執行的,有的時候為了防止在結束mian函數的時候結束掉Goroutine,所以需要同步等待,這個時候就需要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它會等待它收集的所有 goroutine 任務全部完成。在WaitGroup里主要有三個方法:
- Add, 可以添加或減少 goroutine的數量.
- Done, 相當於Add(-1).
- Wait, 執行后會堵塞主線程,直到WaitGroup 里的值減至0.
在主 goroutine 中 Add(delta int) 索要等待goroutine 的數量。 在每一個 goroutine 完成后 Done() 表示這一個goroutine 已經完成,當所有的 goroutine 都完成后,在主 goroutine 中 WaitGroup 返回返回。
func main(){ var wg sync.WaitGroup var urls = []string{ "http://www.golang.org/", "http://www.google.com/", } for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() http.Get(url) }(url) } wg.Wait() }
在Golang官網中對於WaitGroup介紹是A WaitGroup must not be copied after first use
,在 WaitGroup 第一次使用后,不能被拷貝
應用示例:
func main(){ wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) go func(wg sync.WaitGroup, i int) { fmt.Printf("i:%d", i) wg.Done() }(wg, i) } wg.Wait() fmt.Println("exit") }
運行:
i:1i:3i:2i:0i:4fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]: sync.runtime_Semacquire(0xc000094018) /home/keke/soft/go/src/runtime/sema.go:56 +0x39 sync.(*WaitGroup).Wait(0xc000094010) /home/keke/soft/go/src/sync/waitgroup.go:130 +0x64 main.main() /home/keke/go/Test/wait.go:17 +0xab exit status 2
它提示所有的 goroutine 都已經睡眠了,出現了死鎖。這是因為 wg 給拷貝傳遞到了 goroutine 中,導致只有 Add 操作,其實 Done操作是在 wg 的副本執行的。
因此 Wait 就死鎖了。
這個第一個修改方式:將匿名函數中 wg 的傳入類型改為 *sync.WaitGrou,這樣就能引用到正確的WaitGroup了。 這個第二個修改方式:將匿名函數中的 wg 的傳入參數去掉,因為Go支持閉包類型,在匿名函數中可以直接使用外面的 wg 變量
- 在Go 1.7 以后引進的強大的Context上下文,實現並發控制
通常,在一些簡單場景下使用 channel 和 WaitGroup 已經足夠了,但是當面臨一些復雜多變的網絡並發場景下 channel 和 WaitGroup 顯得有些力不從心了。 比如一個網絡請求 Request,每個 Request 都需要開啟一個 goroutine 做一些事情,這些 goroutine 又可能會開啟其他的 goroutine,比如數據庫和RPC服務。 所以我們需要一種可以跟蹤 goroutine 的方案,才可以達到控制他們的目的,這就是Go語言為我們提供的 Context,稱之為上下文非常貼切,它就是goroutine 的上下文。 它是包括一個程序的運行環境、現場和快照等。每個程序要運行時,都需要知道當前程序的運行狀態,通常Go 將這些封裝在一個 Context 里,再將它傳給要執行的 goroutine 。
context 包主要是用來處理多個 goroutine 之間共享數據,及多個 goroutine 的管理。
context 包的核心是 struct Context,接口聲明如下:
// A Context carries a deadline, cancelation signal, and request-scoped values // across API boundaries. Its methods are safe for simultaneous use by multiple // goroutines. type Context interface { // Done returns a channel that is closed when this `Context` is canceled // or times out. Done() <-chan struct{} // Err indicates why this Context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface{}) interface{} }
Done() 返回一個只能接受數據的channel類型,當該context關閉或者超時時間到了的時候,該channel就會有一個取消信號
Err() 在Done() 之后,返回context 取消的原因。
Deadline() 設置該context cancel的時間點
Value() 方法允許 Context 對象攜帶request作用域的數據,該數據必須是線程安全的。
Context 對象是線程安全的,你可以把一個 Context 對象傳遞給任意個數的 gorotuine,對它執行 取消 操作時,所有 goroutine 都會接收到取消信號。
一個 Context 不能擁有 Cancel 方法,同時我們也只能 Done channel 接收數據。 其中的原因是一致的:接收取消信號的函數和發送信號的函數通常不是一個。 典型的場景是:父操作為子操作操作啟動 goroutine,子操作也就不能取消父操作。
5. JSON 標准庫對 nil slice 和 空 slice 的處理是一致的嗎?
首先JSON 標准庫對 nil slice 和 空 slice 的處理是不一致.
通常錯誤的用法,會報數組越界的錯誤,因為只是聲明了slice,卻沒有給實例化的對象。
var slice []int slice[1] = 0
此時slice的值是nil,這種情況可以用於需要返回slice的函數,當函數出現異常的時候,保證函數依然會有nil的返回值。
empty slice 是指slice不為nil,但是slice沒有值,slice的底層的空間是空的,此時的定義如下:
slice := make([]int,0) slice := []int{}
當我們查詢或者處理一個空的列表的時候,這非常有用,它會告訴我們返回的是一個列表,但是列表內沒有任何值。
總之,nil slice 和 empty slice是不同的東西,需要我們加以區分的.
6. 協程,線程,進程的區別。
- 進程
進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。每個進程都有自己的獨立內存空間,不同進程通過進程間通信來通信。由於進程比較重量,占據獨立的內存,所以上下文進程間的切換開銷(棧、寄存器、虛擬內存、文件句柄等)比較大,但相對比較穩定安全。
- 線程
線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。線程間通信主要通過共享內存,上下文切換很快,資源開銷較少,但相比進程不夠穩定容易丟失數據。
- 協程
協程是一種用戶態的輕量級線程,協程的調度完全由用戶控制。協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操作棧則基本沒有內核切換的開銷,可以不加鎖的訪問全局變量,所以上下文的切換非常快。
7. 互斥鎖,讀寫鎖,死鎖問題是怎么解決。
- 互斥鎖
互斥鎖就是互斥變量mutex,用來鎖住臨界區的.
條件鎖就是條件變量,當進程的某些資源要求不滿足時就進入休眠,也就是鎖住了。當資源被分配到了,條件鎖打開,進程繼續運行;讀寫鎖,也類似,用於緩沖區等臨界資源能互斥訪問的。
- 讀寫鎖
通常有些公共數據修改的機會很少,但其讀的機會很多。並且在讀的過程中會伴隨着查找,給這種代碼加鎖會降低我們的程序效率。讀寫鎖可以解決這個問題。
注意:寫獨占,讀共享,寫鎖優先級高
- 死鎖
一般情況下,如果同一個線程先后兩次調用lock,在第二次調用時,由於鎖已經被占用,該線程會掛起等待別的線程釋放鎖,然而鎖正是被自己占用着的,該線程又被掛起而沒有機會釋放鎖,因此就永遠處於掛起等待狀態了,這叫做死鎖(Deadlock)。 另外一種情況是:若線程A獲得了鎖1,線程B獲得了鎖2,這時線程A調用lock試圖獲得鎖2,結果是需要掛起等待線程B釋放鎖2,而這時線程B也調用lock試圖獲得鎖1,結果是需要掛起等待線程A釋放鎖1,於是線程A和B都永遠處於掛起狀態了。
死鎖產生的四個必要條件:
- 互斥條件:一個資源每次只能被一個進程使用
- 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
- 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。 這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。
a. 預防死鎖
可以把資源一次性分配:(破壞請求和保持條件)
然后剝奪資源:即當某進程新的資源未滿足時,釋放已占有的資源(破壞不可剝奪條件)
資源有序分配法:系統給每類資源賦予一個編號,每一個進程按編號遞增的順序請求資源,釋放則相反(破壞環路等待條件)
b. 避免死鎖
預防死鎖的幾種策略,會嚴重地損害系統性能。因此在避免死鎖時,要施加較弱的限制,從而獲得 較滿意的系統性能。由於在避免死鎖的策略中,允許進程動態地申請資源。因而,系統在進行資源分配之前預先計算資源分配的安全性。若此次分配不會導致系統進入不安全狀態,則將資源分配給進程;否則,進程等待。其中最具有代表性的避免死鎖算法是銀行家算法。
c. 檢測死鎖
首先為每個進程和每個資源指定一個唯一的號碼,然后建立資源分配表和進程等待表.
d. 解除死鎖
當發現有進程死鎖后,便應立即把它從死鎖狀態中解脫出來,常采用的方法有.
e. 剝奪資源
從其它進程剝奪足夠數量的資源給死鎖進程,以解除死鎖狀態.
f. 撤消進程
可以直接撤消死鎖進程或撤消代價最小的進程,直至有足夠的資源可用,死鎖狀態.消除為止.所謂代價是指優先級、運行代價、進程的重要性和價值等。
8. Golang的內存模型,為什么小對象多了會造成gc壓力。
通常小對象過多會導致GC三色法消耗過多的GPU。優化思路是,減少對象分配.
9. Data Race問題怎么解決?能不能不加鎖解決這個問題?
同步訪問共享數據是處理數據競爭的一種有效的方法.golang在1.1之后引入了競爭檢測機制,可以使用 go run -race 或者 go build -race來進行靜態檢測。 其在內部的實現是,開啟多個協程執行同一個命令, 並且記錄下每個變量的狀態.
競爭檢測器基於C/C++的ThreadSanitizer 運行時庫,該庫在Google內部代碼基地和Chromium找到許多錯誤。這個技術在2012年九月集成到Go中,從那時開始,它已經在標准庫中檢測到42個競爭條件。現在,它已經是我們持續構建過程的一部分,當競爭條件出現時,它會繼續捕捉到這些錯誤。
競爭檢測器已經完全集成到Go工具鏈中,僅僅添加-race標志到命令行就使用了檢測器。
$ go test -race mypkg // 測試包 $ go run -race mysrc.go // 編譯和運行程序 $ go build -race mycmd // 構建程序 $ go install -race mypkg // 安裝程序
要想解決數據競爭的問題可以使用互斥鎖sync.Mutex,解決數據競爭(Data race),也可以使用管道解決,使用管道的效率要比互斥鎖高.
10. 什么是channel,為什么它可以做到線程安全?
Channel是Go中的一個核心類型,可以把它看成一個管道,通過它並發核心單元就可以發送或者接收數據進行通訊(communication),Channel也可以理解是一個先進先出的隊列,通過管道進行通信。
Golang的Channel,發送一個數據到Channel 和 從Channel接收一個數據 都是 原子性的。而且Go的設計思想就是:不要通過共享內存來通信,而是通過通信來共享內存,前者就是傳統的加鎖,后者就是Channel。也就是說,設計Channel的主要目的就是在多任務間傳遞數據的,這當然是安全的。
11. Epoll原理.
開發高性能網絡程序時,windows開發者們言必稱Iocp,linux開發者們則言必稱Epoll。大家都明白Epoll是一種IO多路復用技術,可以非常高效的處理數以百萬計的Socket句柄,比起以前的Select和Poll效率提高了很多。
先簡單了解下如何使用C庫封裝的3個epoll系統調用。
int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
使用起來很清晰,首先要調用epoll_create
建立一個epoll對象。參數size是內核保證能夠正確處理的最大句柄數,多於這個最大數時內核可不保證效果。 epoll_ctl可以操作上面建立的epoll,例如,將剛建立的socket
加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,不再監控它等等。
epoll_wait
在調用時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就返回用戶態的進程。
從調用方式就可以看到epoll相比select/poll的優越之處是,因為后者每次調用時都要傳遞你所要監控的所有socket給select/poll系統調用,這意味着需要將用戶態的socket列表copy到內核態,如果以萬計的句柄會導致每次都要copy幾十幾百KB的內存到內核態,非常低效。而我們調用epoll_wait
時就相當於以往調用select/poll,但是這時卻不用傳遞socket句柄給內核,因為內核已經在epoll_ctl中拿到了要監控的句柄列表。
所以,實際上在你調用epoll_create
后,內核就已經在內核態開始准備幫你存儲要監控的句柄了,每次調用epoll_ctl
只是在往內核的數據結構里塞入新的socket句柄。
在內核里,一切皆文件。所以,epoll向內核注冊了一個文件系統,用於存儲上述的被監控socket。當你調用epoll_create時,就會在這個虛擬的epoll文件系統里創建一個file結點。當然這個file不是普通文件,它只服務於epoll。
epoll在被內核初始化時(操作系統啟動),同時會開辟出epoll自己的內核高速cache區,用於安置每一個我們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache里,以支持快速的查找、插入、刪除。這個內核高速cache區,就是建立連續的物理內存頁,然后在之上建立slab層,通常來講,就是物理上分配好你想要的size的內存對象,每次使用時都是使用空閑的已分配好的對象。
static int __init eventpoll_init(void) { ... ... /* Allocates slab cache used to allocate "struct epitem" items */ epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem), 0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL); /* Allocates slab cache used to allocate "struct eppoll_entry" */ pwq_cache = kmem_cache_create("eventpoll_pwq", sizeof(struct eppoll_entry), 0, EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL); ... ... }
epoll的高效就在於,當我們調用epoll_ctl
往里塞入百萬個句柄時,epoll_wait
仍然可以飛快的返回,並有效的將發生事件的句柄給我們用戶。這是由於我們在調用epoll_create
時,內核除了幫我們在epoll文件系統里建了個file結點,在內核cache里建了個紅黑樹用於存儲以后epoll_ctl傳來的socket外,還會再建立一個list鏈表,用於存儲准備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表里有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到后即使鏈表沒數據也返回。所以,epoll_wait非常高效。
而且,通常情況下即使我們要監控百萬計的句柄,大多一次也只返回很少量的准備就緒句柄而已,所以,epoll_wait僅需要從內核態copy少量的句柄到用戶態而已,因此就會非常的高效!
然而,這個准備就緒list鏈表是怎么維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統里file對象對應的紅黑樹上之外,還會給內核中斷處理程序注冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到准備就緒list鏈表里。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中后就來把socket插入到准備就緒鏈表里了。
如此,一個紅黑樹,一張准備就緒句柄鏈表,少量的內核cache,就幫我們解決了大並發下的socket處理問題。執行epoll_create
時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內核注冊回調函數,用於當中斷事件來臨時向准備就緒鏈表中插入數據。執行epoll_wait時立刻返回准備就緒鏈表里的數據即可。
最后看看epoll獨有的兩種模式LT和ET。無論是LT和ET模式,都適用於以上所說的流程。區別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在以后調用epoll_wait時每次返回這個句柄,而ET模式僅在第一次返回。
當一個socket句柄上有事件時,內核會把該句柄插入上面所說的准備就緒list鏈表,這時我們調用epoll_wait
,會把准備就緒的socket拷貝到用戶態內存,然后清空准備就緒list鏈表,最后,epoll_wait
需要做的事情,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),並且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的准備就緒鏈表了。所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即使socket上的事件沒有處理完,也是不會每次從epoll_wait返回的。
因此epoll比select的提高實際上是一個用空間換時間思想的具體應用.對比阻塞IO的處理模型, 可以看到采用了多路復用IO之后, 程序可以自由的進行自己除了IO操作之外的工作, 只有到IO狀態發生變化的時候由多路復用IO進行通知, 然后再采取相應的操作, 而不用一直阻塞等待IO狀態發生變化,提高效率.
12. Golang GC 時會發生什么?
首先我們先來了解下垃圾回收.什么是垃圾回收?
內存管理是程序員開發應用的一大難題。傳統的系統級編程語言(主要指C/C++)中,程序開發者必須對內存小心的進行管理操作,控制內存的申請及釋放。因為稍有不慎,就可能產生內存泄露問題,這種問題不易發現並且難以定位,一直成為困擾程序開發者的噩夢。如何解決這個頭疼的問題呢?
過去一般采用兩種辦法:
-
內存泄露檢測工具。這種工具的原理一般是靜態代碼掃描,通過掃描程序檢測可能出現內存泄露的代碼段。然而檢測工具難免有疏漏和不足,只能起到輔助作用。
-
智能指針。這是 c++ 中引入的自動內存管理方法,通過擁有自動內存管理功能的指針對象來引用對象,是程序員不用太關注內存的釋放,而達到內存自動釋放的目的。這種方法是采用最廣泛的做法,但是對程序開發者有一定的學習成本(並非語言層面的原生支持),而且一旦有忘記使用的場景依然無法避免內存泄露。
為了解決這個問題,后來開發出來的幾乎所有新語言(java,python,php等等)都引入了語言層面的自動內存管理 – 也就是語言的使用者只用關注內存的申請而不必關心內存的釋放,內存釋放由虛擬機(virtual machine)或運行時(runtime)來自動進行管理。而這種對不再使用的內存資源進行自動回收的行為就被稱為垃圾回收。
常用的垃圾回收的方法:
- 引用計數(reference counting)
這是最簡單的一種垃圾回收算法,和之前提到的智能指針異曲同工。對每個對象維護一個引用計數,當引用該對象的對象被銷毀或更新時被引用對象的引用計數自動減一,當被引用對象被創建或被賦值給其他對象時引用計數自動加一。當引用計數為0時則立即回收對象。
這種方法的優點是實現簡單,並且內存的回收很及時。這種算法在內存比較緊張和實時性比較高的系統中使用的比較廣泛,如ios cocoa框架,php,python等。
但是簡單引用計數算法也有明顯的缺點:
- 頻繁更新引用計數降低了性能。
一種簡單的解決方法就是編譯器將相鄰的引用計數更新操作合並到一次更新;還有一種方法是針對頻繁發生的臨時變量引用不進行計數,而是在引用達到0時通過掃描堆棧確認是否還有臨時對象引用而決定是否釋放。等等還有很多其他方法,具體可以參考這里。
- 循環引用。
當對象間發生循環引用時引用鏈中的對象都無法得到釋放。最明顯的解決辦法是避免產生循環引用,如cocoa引入了strong指針和weak指針兩種指針類型。或者系統檢測循環引用並主動打破循環鏈。當然這也增加了垃圾回收的復雜度。
- 標記-清除(mark and sweep)
標記-清除(mark and sweep)分為兩步,標記從根變量開始迭代得遍歷所有被引用的對象,對能夠通過應用遍歷訪問到的對象都進行標記為“被引用”;標記完成后進行清除操作,對沒有標記過的內存進行回收(回收同時可能伴有碎片整理操作)。這種方法解決了引用計數的不足,但是也有比較明顯的問題:每次啟動垃圾回收都會暫停當前所有的正常代碼執行,回收是系統響應能力大大降低!當然后續也出現了很多mark&sweep算法的變種(如三色標記法)優化了這個問題。
- 分代搜集(generation)
java的jvm 就使用的分代回收的思路。在面向對象編程語言中,絕大多數對象的生命周期都非常短。分代收集的基本思想是,將堆划分為兩個或多個稱為代(generation)的空間。新創建的對象存放在稱為新生代(young generation)中(一般來說,新生代的大小會比 老年代小很多),隨着垃圾回收的重復執行,生命周期較長的對象會被提升(promotion)到老年代中(這里用到了一個分類的思路,這個是也是科學思考的一個基本思路)。
因此,新生代垃圾回收和老年代垃圾回收兩種不同的垃圾回收方式應運而生,分別用於對各自空間中的對象執行垃圾回收。新生代垃圾回收的速度非常快,比老年代快幾個數量級,即使新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是因為大多數對象的生命周期都很短,根本無需提升到老年代。
Golang GC 時會發生什么?
Golang 1.5后,采取的是“非分代的、非移動的、並發的、三色的”標記清除垃圾回收算法。
golang 中的 gc 基本上是標記清除的過程:
gc的過程一共分為四個階段:
- 棧掃描(開始時STW)
- 第一次標記(並發)
- 第二次標記(STW)
- 清除(並發)
整個進程空間里申請每個對象占據的內存可以視為一個圖,初始狀態下每個內存對象都是白色標記。
- 先STW,做一些准備工作,比如 enable write barrier。然后取消STW,將掃描任務作為多個並發的goroutine立即入隊給調度器,進而被CPU處理
- 第一輪先掃描root對象,包括全局指針和 goroutine 棧上的指針,標記為灰色放入隊列
- 第二輪將第一步隊列中的對象引用的對象置為灰色加入隊列,一個對象引用的所有對象都置灰並加入隊列后,這個對象才能置為黑色並從隊列之中取出。循環往復,最后隊列為空時,整個圖剩下的白色內存空間即不可到達的對象,即沒有被引用的對象;
- 第三輪再次STW,將第二輪過程中新增對象申請的內存進行標記(灰色),這里使用了write barrier(寫屏障)去記錄
Golang gc 優化的核心就是盡量使得 STW(Stop The World) 的時間越來越短。
詳細的Golang的GC介紹可以參看Golang垃圾回收.
13. Golang 中 Goroutine 如何調度?
goroutine是Golang語言中最經典的設計,也是其魅力所在,goroutine的本質是協程,是實現並行計算的核心。 goroutine使用方式非常的簡單,只需使用go關鍵字即可啟動一個協程,並且它是處於異步方式運行,你不需要等它運行完成以后在執行以后的代碼。
go func()//通過go關鍵字啟動一個協程來運行函數
協程:
協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。 因此,協程能保留上一次調用時的狀態(即所有局部狀態的一個特定組合),每次過程重入時,就相當於進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。 線程和進程的操作是由程序觸發系統接口,最后的執行者是系統;協程的操作執行者則是用戶自身程序,goroutine也是協程。
groutine能擁有強大的並發實現是通過GPM調度模型實現.
Go的調度器內部有四個重要的結構:M,P,S,Sched,如上圖所示(Sched未給出).
- M:M代表內核級線程,一個M就是一個線程,goroutine就是跑在M之上的;M是一個很大的結構,里面維護小對象內存cache(mcache)、當前執行的goroutine、隨機數發生器等等非常多的信息
- G:代表一個goroutine,它有自己的棧,instruction pointer和其他信息(正在等待的channel等等),用於調度。
- P:P全稱是Processor,處理器,它的主要用途就是用來執行goroutine的,所以它也維護了一個goroutine隊列,里面存儲了所有需要它來執行的goroutine
- Sched:代表調度器,它維護有存儲M和G的隊列以及調度器的一些狀態信息等。
調度實現:
從上圖中可以看到,有2個物理線程M,每一個M都擁有一個處理器P,每一個也都有一個正在運行的goroutine。P的數量可以通過GOMAXPROCS()來設置,它其實也就代表了真正的並發度,即有多少個goroutine可以同時運行。
圖中灰色的那些goroutine並沒有運行,而是出於ready的就緒態,正在等待被調度。P維護着這個隊列(稱之為runqueue),Go語言里,啟動一個goroutine很容易:go function 就行,所以每有一個go語句被執行,runqueue隊列就在其末尾加入一個goroutine,在下一個調度點,就從runqueue中取出(如何決定取哪個goroutine?)一個goroutine執行。
當一個OS線程M0陷入阻塞時,P轉而在運行M1,圖中的M1可能是正被創建,或者從線程緩存中取出。
當MO返回時,它必須嘗試取得一個P來運行goroutine,一般情況下,它會從其他的OS線程那里拿一個P過來, 如果沒有拿到的話,它就把goroutine放在一個global runqueue里,然后自己睡眠(放入線程緩存里)。所有的P也會周期性的檢查global runqueue並運行其中的goroutine,否則global runqueue上的goroutine永遠無法執行。
另一種情況是P所分配的任務G很快就執行完了(分配不均),這就導致了這個處理器P很忙,但是其他的P還有任務,此時如果global runqueue沒有任務G了,那么P不得不從其他的P里拿一些G來執行。
通常來說,如果P從其他的P那里要拿任務的話,一般就拿run queue的一半,這就確保了每個OS線程都能充分的使用。
14. 並發編程概念是什么?
並行是指兩個或者多個事件在同一時刻發生;並發是指兩個或多個事件在同一時間間隔發生。
並行是在不同實體上的多個事件,並發是在同一實體上的多個事件。在一台處理器上“同時”處理多個任務,在多台處理器上同時處理多個任務。如hadoop分布式集群
並發偏重於多個任務交替執行,而多個任務之間有可能還是串行的。而並行是真正意義上的“同時執行”。
並發編程是指在一台處理器上“同時”處理多個任務。並發是在同一實體上的多個事件。多個事件在同一時間間隔發生。並發編程的目標是充分的利用處理器的每一個核,以達到最高的處理性能。
15. 負載均衡原理是什么?
負載均衡Load Balance)是高可用網絡基礎架構的關鍵組件,通常用於將工作負載分布到多個服務器來提高網站、應用、數據庫或其他服務的性能和可靠性。負載均衡,其核心就是網絡流量分發,分很多維度。
負載均衡(Load Balance)通常是分攤到多個操作單元上進行執行,例如Web服務器、FTP服務器、企業關鍵應用服務器和其它關鍵任務服務器等,從而共同完成工作任務。
負載均衡是建立在現有網絡結構之上,它提供了一種廉價有效透明的方法擴展網絡設備和服務器的帶寬、增加吞吐量、加強網絡數據處理能力、提高網絡的靈活性和可用性。
通過一個例子詳細介紹:
- 沒有負載均衡 web 架構
在這里用戶是直連到 web 服務器,如果這個服務器宕機了,那么用戶自然也就沒辦法訪問了。 另外,如果同時有很多用戶試圖訪問服務器,超過了其能處理的極限,就會出現加載速度緩慢或根本無法連接的情況。
而通過在后端引入一個負載均衡器和至少一個額外的 web 服務器,可以緩解這個故障。 通常情況下,所有的后端服務器會保證提供相同的內容,以便用戶無論哪個服務器響應,都能收到一致的內容。
- 有負載均衡 web 架構
用戶訪問負載均衡器,再由負載均衡器將請求轉發給后端服務器。在這種情況下,單點故障現在轉移到負載均衡器上了。 這里又可以通過引入第二個負載均衡器來緩解。
那么負載均衡器的工作方式是什么樣的呢,負載均衡器又可以處理什么樣的請求?
負載均衡器的管理員能主要為下面四種主要類型的請求設置轉發規則:
- HTTP (七層)
- HTTPS (七層)
- TCP (四層)
- UDP (四層)
負載均衡器如何選擇要轉發的后端服務器?
負載均衡器一般根據兩個因素來決定要將請求轉發到哪個服務器。首先,確保所選擇的服務器能夠對請求做出響應,然后根據預先配置的規則從健康服務器池(healthy pool)中進行選擇。
因為,負載均衡器應當只選擇能正常做出響應的后端服務器,因此就需要有一種判斷后端服務器是否健康的方法。為了監視后台服務器的運行狀況,運行狀態檢查服務會定期嘗試使用轉發規則定義的協議和端口去連接后端服務器。 如果,服務器無法通過健康檢查,就會從池中剔除,保證流量不會被轉發到該服務器,直到其再次通過健康檢查為止。
負載均衡算法
負載均衡算法決定了后端的哪些健康服務器會被選中。 其中常用的算法包括:
- Round Robin(輪詢):為第一個請求選擇列表中的第一個服務器,然后按順序向下移動列表直到結尾,然后循環。
- Least Connections(最小連接):優先選擇連接數最少的服務器,在普遍會話較長的情況下推薦使用。
- Source:根據請求源的 IP 的散列(hash)來選擇要轉發的服務器。這種方式可以一定程度上保證特定用戶能連接到相同的服務器。
如果你的應用需要處理狀態而要求用戶能連接到和之前相同的服務器。可以通過 Source 算法基於客戶端的 IP 信息創建關聯,或者使用粘性會話(sticky sessions)。
除此之外,想要解決負載均衡器的單點故障問題,可以將第二個負載均衡器連接到第一個上,從而形成一個集群。
16. LVS相關了解.
LVS是 Linux Virtual Server 的簡稱,也就是Linux虛擬服務器。這是一個由章文嵩博士發起的一個開源項目,它的官方網站是LinuxVirtualServer現在 LVS 已經是 Linux 內核標准的一部分。使用 LVS 可以達到的技術目標是:通過 LVS 達到的負載均衡技術和 Linux 操作系統實現一個高性能高可用的 Linux 服務器集群,它具有良好的可靠性、可擴展性和可操作性。 從而以低廉的成本實現最優的性能。LVS 是一個實現負載均衡集群的開源軟件項目,LVS架構從邏輯上可分為調度層、Server集群層和共享存儲。
LVS的基本工作原理:
- 當用戶向負載均衡調度器(Director Server)發起請求,調度器將請求發往至內核空間
- PREROUTING鏈首先會接收到用戶請求,判斷目標IP確定是本機IP,將數據包發往INPUT鏈
- IPVS是工作在INPUT鏈上的,當用戶請求到達INPUT時,IPVS會將用戶請求和自己已定義好的集群服務進行比對,如果用戶請求的就是定義的集群服務,那么此時IPVS會強行修改數據包里的目標IP地址及端口,並將新的數據包發往POSTROUTING鏈
- POSTROUTING鏈接收數據包后發現目標IP地址剛好是自己的后端服務器,那么此時通過選路,將數據包最終發送給后端的服務器
LVS的組成:
LVS 由2部分程序組成,包括 ipvs
和 ipvsadm
。
- ipvs(ip virtual server):一段代碼工作在內核空間,叫ipvs,是真正生效實現調度的代碼。
- ipvsadm:另外一段是工作在用戶空間,叫ipvsadm,負責為ipvs內核框架編寫規則,定義誰是集群服務,而誰是后端真實的服務器(Real Server)
詳細的LVS的介紹可以參考LVS詳解.
17. 微服務架構是什么樣子的?
通常傳統的項目體積龐大,需求、設計、開發、測試、部署流程固定。新功能需要在原項目上做修改。
但是微服務可以看做是對大項目的拆分,是在快速迭代更新上線的需求下產生的。新的功能模塊會發布成新的服務組件,與其他已發布的服務組件一同協作。 服務內部有多個生產者和消費者,通常以http rest的方式調用,服務總體以一個(或幾個)服務的形式呈現給客戶使用。
微服務架構是一種思想對微服務架構我們沒有一個明確的定義,但簡單來說微服務架構是:
采用一組服務的方式來構建一個應用,服務獨立部署在不同的進程中,不同服務通過一些輕量級交互機制來通信,例如 RPC、HTTP 等,服務可獨立擴展伸縮,每個服務定義了明確的邊界,不同的服務甚至可以采用不同的編程語言來實現,由獨立的團隊來維護。
Golang的微服務框架kit中有詳細的微服務的例子,可以參考學習.
微服務架構設計包括:
- 服務熔斷降級限流機制 熔斷降級的概念(Rate Limiter 限流器,Circuit breaker 斷路器).
- 框架調用方式解耦方式 Kit 或 Istio 或 Micro 服務發現(consul zookeeper kubeneters etcd ) RPC調用框架.
- 鏈路監控,zipkin和prometheus.
- 多級緩存.
- 網關 (kong gateway).
- Docker部署管理 Kubenetters.
- 自動集成部署 CI/CD 實踐.
- 自動擴容機制規則.
- 壓測 優化.
- Trasport 數據傳輸(序列化和反序列化).
- Logging 日志.
- Metrics 指針對每個請求信息的儀表盤化.
微服務架構介紹詳細的可以參考:
18. 分布式鎖實現原理,用過嗎?
在分析分布式鎖的三種實現方式之前,先了解一下分布式鎖應該具備哪些條件:
- 在分布式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行;
- 高可用的獲取鎖與釋放鎖;
- 高性能的獲取鎖與釋放鎖;
- 具備可重入特性;
- 具備鎖失效機制,防止死鎖;
- 具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。
分布式的CAP理論告訴我們“任何一個分布式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多只能同時滿足兩項。”所以,很多系統在設計之初就要對這三者做出取舍。在互聯網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證“最終一致性”,只要這個最終時間是在用戶可以接受的范圍內即可。
通常分布式鎖以單獨的服務方式實現,目前比較常用的分布式鎖實現有三種:
- 基於數據庫實現分布式鎖。
- 基於緩存(redis,memcached,tair)實現分布式鎖。
- 基於Zookeeper實現分布式鎖。
盡管有這三種方案,但是不同的業務也要根據自己的情況進行選型,他們之間沒有最好只有更適合!
- 基於數據庫的實現方式
基於數據庫的實現方式的核心思想是:在數據庫中創建一個表,表中包含方法名等字段,並在方法名字段上創建唯一索引,想要執行某個方法,就使用這個方法名向表中插入數據,成功插入則獲取鎖,執行完成后刪除對應的行數據釋放鎖。
創建一個表:
DROP TABLE IF EXISTS `method_lock`; CREATE TABLE `method_lock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `method_name` varchar(64) NOT NULL COMMENT '鎖定的方法名', `desc` varchar(255) NOT NULL COMMENT '備注信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
想要執行某個方法,就使用這個方法名向表中插入數據:
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '測試的methodName');
因為我們對method_name做了唯一性約束,這里如果有多個請求同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,那么我們就可以認為操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。
成功插入則獲取鎖,執行完成后刪除對應的行數據釋放鎖:
delete from method_lock where method_name ='methodName';
注意:這里只是使用基於數據庫的一種方法,使用數據庫實現分布式鎖還有很多其他的用法可以實現!
使用基於數據庫的這種實現方式很簡單,但是對於分布式鎖應該具備的條件來說,它有一些問題需要解決及優化:
1、因為是基於數據庫實現的,數據庫的可用性和性能將直接影響分布式鎖的可用性及性能,所以,數據庫需要雙機部署、數據同步、主備切換;
2、不具備可重入的特性,因為同一個線程在釋放鎖之前,行數據一直存在,無法再次成功插入數據,所以,需要在表中新增一列,用於記錄當前獲取到鎖的機器和線程信息,在再次獲取鎖的時候,先查詢表中機器和線程信息是否和當前機器和線程相同,若相同則直接獲取鎖;
3、沒有鎖失效機制,因為有可能出現成功插入數據后,服務器宕機了,對應的數據沒有被刪除,當服務恢復后一直獲取不到鎖,所以,需要在表中新增一列,用於記錄失效時間,並且需要有定時任務清除這些失效的數據;
4、不具備阻塞鎖特性,獲取不到鎖直接返回失敗,所以需要優化獲取邏輯,循環多次去獲取。
5、在實施的過程中會遇到各種不同的問題,為了解決這些問題,實現方式將會越來越復雜;依賴數據庫需要一定的資源開銷,性能問題需要考慮。
- 基於Redis的實現方式
選用Redis實現分布式鎖原因:
- Redis有很高的性能;
- Redis命令對此支持較好,實現起來比較方便
主要實現方式:
- SET lock currentTime+expireTime EX 600 NX,使用set設置lock值,並設置過期時間為600秒,如果成功,則獲取鎖;
- 獲取鎖后,如果該節點掉線,則到過期時間ock值自動失效;
- 釋放鎖時,使用del刪除lock鍵值;
使用redis單機來做分布式鎖服務,可能會出現單點問題,導致服務可用性差,因此在服務穩定性要求高的場合,官方建議使用redis集群(例如5台,成功請求鎖超過3台就認為獲取鎖),來實現redis分布式鎖。詳見RedLock。
優點:性能高,redis可持久化,也能保證數據不易丟失,redis集群方式提高穩定性。
缺點:使用redis主從切換時可能丟失部分數據。
- 基於ZooKeeper的實現方式
ZooKeeper是一個為分布式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個唯一文件名。基於ZooKeeper實現分布式鎖的步驟如下:
- 創建一個目錄mylock;
- 線程A想獲取鎖就在mylock目錄下創建臨時順序節點;
- 獲取mylock目錄下所有的子節點,然后獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖;
- 線程B獲取所有節點,判斷自己不是最小節點,設置監聽比自己次小的節點;
- 線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。
這里推薦一個Apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分布式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。
優點:具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。
缺點:因為需要頻繁的創建和刪除節點,性能上不如Redis方式。
上面的三種實現方式,沒有在所有場合都是完美的,所以,應根據不同的應用場景選擇最適合的實現方式。
在分布式環境中,對資源進行上鎖有時候是很重要的,比如搶購某一資源,這時候使用分布式鎖就可以很好地控制資源。
19. Etcd怎么實現分布式鎖?
首先思考下Etcd是什么?可能很多人第一反應可能是一個鍵值存儲倉庫,卻沒有重視官方定義的后半句,用於配置共享和服務發現。
A highly-available key value store for shared configuration and service discovery.
實際上,etcd 作為一個受到 ZooKeeper 與 doozer 啟發而催生的項目,除了擁有與之類似的功能外,更專注於以下四點。
- 簡單:基於 HTTP+JSON 的 API 讓你用 curl 就可以輕松使用。
- 安全:可選 SSL 客戶認證機制。
- 快速:每個實例每秒支持一千次寫操作。
- 可信:使用 Raft 算法充分實現了分布式。
但是這里我們主要講述Etcd如何實現分布式鎖?
因為 Etcd 使用 Raft 算法保持了數據的強一致性,某次操作存儲到集群中的值必然是全局一致的,所以很容易實現分布式鎖。鎖服務有兩種使用方式,一是保持獨占,二是控制時序。
-
保持獨占即所有獲取鎖的用戶最終只有一個可以得到。etcd 為此提供了一套實現分布式鎖原子操作 CAS(CompareAndSwap)的 API。通過設置prevExist值,可以保證在多個節點同時去創建某個目錄時,只有一個成功。而創建成功的用戶就可以認為是獲得了鎖。
-
控制時序,即所有想要獲得鎖的用戶都會被安排執行,但是獲得鎖的順序也是全局唯一的,同時決定了執行順序。etcd 為此也提供了一套 API(自動創建有序鍵),對一個目錄建值時指定為POST動作,這樣 etcd 會自動在目錄下生成一個當前最大的值為鍵,存儲這個新的值(客戶端編號)。同時還可以使用 API 按順序列出所有當前目錄下的鍵值。此時這些鍵的值就是客戶端的時序,而這些鍵中存儲的值可以是代表客戶端的編號。
在這里Ectd實現分布式鎖基本實現原理為:
- 在ectd系統里創建一個key
- 如果創建失敗,key存在,則監聽該key的變化事件,直到該key被刪除,回到1
- 如果創建成功,則認為我獲得了鎖
應用示例:
package etcdsync
import ( "fmt" "io" "os" "sync" "time" "github.com/coreos/etcd/client" "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context" ) const ( defaultTTL = 60 defaultTry = 3 deleteAction = "delete" expireAction = "expire" ) // A Mutex is a mutual exclusion lock which is distributed across a cluster. type Mutex struct { key string id string // The identity of the caller client client.Client kapi client.KeysAPI ctx context.Context ttl time.Duration mutex *sync.Mutex logger io.Writer } // New creates a Mutex with the given key which must be the same // across the cluster nodes. // machines are the ectd cluster addresses func New(key string, ttl int, machines []string) *Mutex { cfg := client.Config{ Endpoints: machines, Transport: client.DefaultTransport, HeaderTimeoutPerRequest: time.Second, } c, err := client.New(cfg) if err != nil { return nil } hostname, err := os.Hostname() if err != nil { return nil } if len(key) == 0 || len(machines) == 0 { return nil } if key[0] != '/' { key = "/" + key } if ttl < 1 { ttl = defaultTTL } return &Mutex{ key: key, id: fmt.Sprintf("%v-%v-%v", hostname, os.Getpid(), time.Now().Format("20060102-15:04:05.999999999")), client: c, kapi: client.NewKeysAPI(c), ctx: context.TODO(), ttl: time.Second * time.Duration(ttl), mutex: new(sync.Mutex), } } // Lock locks m. // If the lock is already in use, the calling goroutine // blocks until the mutex is available. func (m *Mutex) Lock() (err error) { m.mutex.Lock() for try := 1; try <= defaultTry; try++ { if m.lock() == nil { return nil } m.debug("Lock node %v ERROR %v", m.key, err) if try < defaultTry { m.debug("Try to lock node %v again", m.key, err) } } return err } func (m *Mutex) lock() (err error) { m.debug("Trying to create a node : key=%v", m.key) setOptions := &client.SetOptions{ PrevExist:client.PrevNoExist, TTL: m.ttl, } resp, err := m.kapi.Set(m.ctx, m.key, m.id, setOptions) if err == nil { m.debug("Create node %v OK [%q]", m.key, resp) return nil } m.debug("Create node %v failed [%v]", m.key, err) e, ok := err.(client.Error) if !ok { return err } if e.Code != client.ErrorCodeNodeExist { return err } // Get the already node's value. resp, err = m.kapi.Get(m.ctx, m.key, nil) if err != nil { return err } m.debug("Get node %v OK", m.key) watcherOptions := &client.WatcherOptions{ AfterIndex : resp.Index, Recursive:false, } watcher := m.kapi.Watcher(m.key, watcherOptions) for { m.debug("Watching %v ...", m.key) resp, err = watcher.Next(m.