Goroutine的上下文儲存(續)
要做Goroutine級別的儲存,首先是要獲取到Goroutine的標識,之前提到過獲取routine id的兩個庫,效率也比較低下,用在效能要求比較苛刻的場景下並不適合。
最近看到有個通過go彙編獲取goid的方法, ofollow,noindex">https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-08-goroutine-id.html 。原理其實很簡單,golang的內部其實是儲存了routineid的,放在了runtime2.go檔案的g struct中,只不過這個struct是私有變數,不可以通過外部訪問。通過go彙編,我們可以繞過go語言層級帶來的障礙,直接訪問到物件所在的記憶體。
獲取到goid以後,我們就可以通過map來儲存上下文了。
新的問題是,golang的map並不是執行緒安全,併發的讀寫會產生問題。實現過程中需要考慮多執行緒的安全問題。原文提供的方案比較簡單,就是在讀寫過程中對map加互斥鎖。進一步的優化方案是使用RWMutex,讀map的時候加RLock,寫操作加互斥鎖,這個也是golang官方推薦的方案, https://blog.golang.org/go-maps-in-action 。
golang的sync包下面,也有一套map的同步方案,sync/map.go,那麼這個方法又有什麼特點,我們該怎麼選擇呢?看了原始碼以後,發現這一套方案主要是通過空間換時間的方法來減少鎖的使用,內部通過兩個map儲存資料,老的資料存放在read map,新資料存放dirty map,如果資料特徵是一次寫入,多次讀出,那麼多數的讀請求都會落入read map,不需要加鎖,從而提升併發效能。
那麼,實際過程中,這三種方案的效能表現到底如何呢?我對不同的讀寫比例和不同的併發程度做了benchmark,詳情見下圖。直接上結論,在讀寫比例100:1以下時RWMutex方案有絕對優勢,更高的讀寫比例下,sync.Map的方案具有更高的效能;RWMutex和sync.Map的效能在大部分情況下都比單鎖的方案高,併發程度越高,優勢越明顯。

10次讀操作

100次讀操作

1000次讀操作
回到我們的場景,上下文儲存經常用來處理http server請求相關的資料,減少層層傳參。這種場景下,讀寫比例不會非常高,所以一般來說RWMutex方案是最優的選擇。
實現程式碼和bechmark程式碼見github, https://github.com/JasonYuan/gls.git 。
另,在看sync.Map的過程中,順便看了下sync.atomic,顧名思義,這是保證各種資料操作原子性的的庫。看到一個有意思的事情,在amd64下,大部分的內建型別實際上都可以保證寫入的原子性,但是interface型別是不能保證的,原因是interface的內部儲存是個struct,同時儲存了型別和地址。這時候可以通過atomic.Value來實現,實現過程無鎖,可以放心服用。