這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Golang的提供的同步機制有sync模組下的Mutex、WaitGroup以及語言自身提供的chan等。這些同步的方法都是以runtime中實現的底層同步機制(cas、atomic、spinlock、sem)為基礎的,本文主要探討Golang底層的同步機制如何?。
1 cas、atomic
cas(Compare And Swap)和原子運算是其他同步機制的基礎,在runtime/asm_xxx.s(xxx代表系統架構,比如amd64)中實現。amd64架構的系統中,主要通過兩條彙編語句來實現,一個是LOCK、一個是CMPXCHG。
LOCK是一個指令首碼,其後必須跟一條“讀-改-寫”的指令,比如INC、XCHG、CMPXCHG等。這條指令對CPU緩衝的訪問將是排他的。
CMPXCHG是完成CAS動作的指令。把LOCK和CMPXCHG一起使用,就達到了原子CAS的功能。
atomic操作也是通過LOCK和其他算術操作(XADD、ORB等)組合來實現。
2 自旋鎖
Golang中的自旋鎖用來實現其他類型的鎖,自旋鎖的作用和互斥量類似,不同點在於,它不是通過休眠來使進程阻塞,而是在獲得鎖之前一直處於忙等狀態(自旋),從而避免了進程(或者
和自旋鎖相關的函數有sync_runtime_canSpin和sync_runtime_doSpin,前者用來判斷當前是否可以進行自旋,後者執行自旋操作。二者通常一起使用。
sync_runtime_canSpin函數中在以下四種情況返回false
- 已經執行了很多次
- 是單核CPU
- 沒有其他正在啟動並執行P
- 當前P的G隊列為空白
條件1避免長時間自旋浪費CPU的情況。
條件2、3用來保證除了當前在啟動並執行Goroutine之外,還有其他Goroutine在運行。
條件4是避免自旋鎖等待的條件是由當前P的其他G來觸發,這樣會導致在自旋變得沒有意義,因為條件永遠無法觸發。
sync_runtime_doSpin會調用procyield函數,該函數也是組合語言實現。函數內部迴圈調用PAUSE指令。PAUSE指令什麼都不做,但是會消耗CPU時間,在執行PAUSE指令時,CPU不會對他做不必要的最佳化。
3 訊號量
按照runtime/sema.go中的注釋:
Think of them as a way to implement sleep and wakeup
Golang中的sema,提供了休眠和喚醒Goroutine的功能。
semacquire函數首先檢查訊號量是否為0:如果大於0,讓訊號量減一,返回;如果等於0,就調用goparkunlock函數,把當前Goroutine放入該sema的等待隊列,並把他設為等待狀態。
semrelease函數首先讓訊號量加一,然後檢查是否有正在等待的Goroutine:如果沒有,直接返回;如果有,調用goready函數喚醒一個Goroutine。
4 sync/Mutex
Mutex擁有Lock、Unlock兩個方法,主要的實現思想都體現在Lock函數中。
Lock執行時,分三種情況:
- 無衝突 通過CAS操作把目前狀態設定為加鎖狀態;
- 有衝突 開始自旋,並等待鎖釋放,如果其他Goroutine在這段時間內釋放了該鎖,直接獲得該鎖;如果沒有釋放,進入3;
- 有衝突,且已經過了自旋階段 通過調用semacquire函數來讓當前Goroutine進入等待狀態。
無衝突時是最簡單的情況;有衝突時,首先進行自旋,是從效率方面考慮的,因為大多數的Mutex保護的程式碼片段都很短,經過短暫的自旋就可以獲得;如果自旋等待無果,就只好通過訊號量來讓當前Goroutine進入等待了。