從2013年起,經(jīng)朋友推薦開(kāi)始用Golang編寫(xiě)游戲登陸服務(wù)器, 配合C++做第三方平臺(tái)驗(yàn)證. 到編寫(xiě)?yīng)毩⒐ぞ邔?dǎo)表工具GitHub - davyxu/tabtoy: 跨平臺(tái)的高性能便捷電子表格導(dǎo)出器. 以及網(wǎng)絡(luò)庫(kù)GitHub - davyxu/cellnet: 簡(jiǎn)單,方便,高效的Go語(yǔ)言的游戲服務(wù)器底層. 最終使用這些工具及庫(kù)編寫(xiě)整個(gè)游戲服務(wù)器框架, 我的感受是很不錯(cuò)的
創(chuàng)新互聯(lián)公司IDC提供業(yè)務(wù):成都移動(dòng)機(jī)房托管,成都服務(wù)器租用,成都移動(dòng)機(jī)房托管,重慶服務(wù)器租用等四川省內(nèi)主機(jī)托管與主機(jī)租用業(yè)務(wù);數(shù)據(jù)中心含:雙線(xiàn)機(jī)房,BGP機(jī)房,電信機(jī)房,移動(dòng)機(jī)房,聯(lián)通機(jī)房。
細(xì)節(jié)看來(lái), 有如下的幾個(gè)點(diǎn):
語(yǔ)言, 庫(kù)
Golang語(yǔ)言特性和C很像, 簡(jiǎn)單, 一張A4紙就能寫(xiě)完所有特性. 你想想看, C++到了領(lǐng)悟階段, 也只用那幾個(gè)簡(jiǎn)單特性, 剩下的都是一大堆解決各種內(nèi)存問(wèn)題的技巧. 而Golang一開(kāi)始就簡(jiǎn)單, 何必浪費(fèi)生命去研究那一大堆的奇技淫巧呢?
Golang的坑只有2個(gè):1. interface{}和nil配合使用, 2. for循環(huán)時(shí), 將循環(huán)變量引入閉包(Golang, Lua, C#閉包變量捕獲差異) 完全不影響正常使用, 復(fù)合語(yǔ)言概念, 只是看官方后面怎么有效的避免
用Golang就忘記繼承那套東西, 用組合+接口
用Golang服務(wù)器如何保證解決游戲服務(wù)器存盤(pán)一致性問(wèn)題? stop the world是肯定的, 但是Golang可以從語(yǔ)言層并發(fā)序列化玩家數(shù)據(jù), 再通過(guò)后臺(tái)存盤(pán)
channel是goroutine雖然是Golang的語(yǔ)言特性. 但是在編寫(xiě)服務(wù)器時(shí), 其實(shí)只有底層用的比較多.
Golang的第三方庫(kù)簡(jiǎn)直多如牛毛, 好的也很多
不要說(shuō)模板了, C#的也不好用, 官方在糾結(jié)也不要加, 使用中, 沒(méi)模板確實(shí)有點(diǎn)不方便. 用interface{}/反射做泛型對(duì)于Golang這種強(qiáng)類(lèi)型語(yǔ)言來(lái)說(shuō),還是有點(diǎn)打臉
運(yùn)行期
Golang和C++比性能的話(huà), 這是C++的優(yōu)勢(shì), Golang因?yàn)闆](méi)虛擬機(jī), 只有薄薄的一層調(diào)度層. 因此性能是非常高的, 用一點(diǎn)性能犧牲換開(kāi)發(fā)效率, 妥妥的
1.6版后的GC優(yōu)化的已經(jīng)很好了, 如果你不是高性能,高并發(fā)Web應(yīng)用, 非要找出一堆的優(yōu)化技巧的話(huà). 只用Golang寫(xiě)點(diǎn)游戲服務(wù)器, 那點(diǎn)GC損耗可以忽略不計(jì)
和其他現(xiàn)代語(yǔ)言一樣, 崩潰捕捉是標(biāo)配功能, 我用Golang的服務(wù)器線(xiàn)上跑, 基本沒(méi)碰到過(guò)崩潰情況
熱更新: 官方已經(jīng)有plugin系統(tǒng)的提交, 跨平臺(tái)的. 估計(jì)很快就可以告別手動(dòng)cgo做so熱更新
開(kāi)發(fā), 調(diào)試, 部署, 優(yōu)化
LiteIDE是我首選的Golang的IDE, 雖然有童鞋說(shuō)B格不高. 但這估計(jì)實(shí)在是找不到缺點(diǎn)說(shuō)了, 別跟我說(shuō)Visual Studio, 那是宇宙級(jí)的...
曾經(jīng)聽(tīng)說(shuō)有人不看好Golang, 我問(wèn)為啥: 說(shuō)這么新的語(yǔ)言, 不好招人,后面打聽(tīng)到他是個(gè)策劃... 好吧
真實(shí)情況是這樣的: Golang對(duì)于有點(diǎn)編程基礎(chǔ)的新人來(lái)說(shuō), 1周左右可以開(kāi)始貢獻(xiàn)代碼. 老司機(jī)2~3天.
開(kāi)發(fā)效率還是不錯(cuò)的, 一般大的游戲功能, 2*2人一周3~4個(gè)整完. 這換C++時(shí)代, 大概也就1~2個(gè)還寫(xiě)不完. 對(duì)接服務(wù)器sdk的話(huà), 大概1天接個(gè)10多個(gè)沒(méi)問(wèn)題
Golang自帶性能調(diào)優(yōu)工具, 從內(nèi)存, CPU, 阻塞點(diǎn)等幾個(gè)方面直接出圖進(jìn)行分析, 非常直觀, 可以參考我博客幾年前的分析: 使用Golang進(jìn)行性能分析(Profiling)
Golang支持交叉編譯, 跨平臺(tái)部署, 什么概念? linux是吧? 不問(wèn)你什么版本, 直接windows上編譯輸出一個(gè)elf, 甩到服務(wù)器上開(kāi)跑.不超過(guò)1分鐘時(shí)間..
前段時(shí)間在golang-China讀到這個(gè)貼:
個(gè)人覺(jué)得golang十分適合進(jìn)行網(wǎng)游服務(wù)器端開(kāi)發(fā),寫(xiě)下這篇文章總結(jié)一下。
從網(wǎng)游的角度看:
要成功的運(yùn)營(yíng)一款網(wǎng)游,很大程度上依賴(lài)于玩家自發(fā)形成的社區(qū)。只有玩家自發(fā)形成一個(gè)穩(wěn)定的生態(tài)系統(tǒng),游戲才能持續(xù)下去,避免鬼城的出現(xiàn)。而這就需要多次大量導(dǎo)入用戶(hù),在同時(shí)在線(xiàn)用戶(hù)量達(dá)到某個(gè)臨界點(diǎn)的時(shí)候,才有可能完成。因此,多人同時(shí)在線(xiàn)十分有必要。
再來(lái)看網(wǎng)游的常見(jiàn)玩法,除了排行榜這類(lèi)統(tǒng)計(jì)和數(shù)據(jù)匯總的功能外,基本沒(méi)有需要大量CPU時(shí)間的應(yīng)用。以前的項(xiàng)目里,即時(shí)戰(zhàn)斗產(chǎn)生的各種傷害計(jì)算對(duì)CPU的消耗也不大。玩家要完成一次操作,需要通過(guò)客戶(hù)端-服務(wù)器端-客戶(hù)端這樣一個(gè)來(lái)回,為了獲得高響應(yīng)速度,滿(mǎn)足玩家體驗(yàn),服務(wù)器端的處理也不能占用太多時(shí)間。所以,每次請(qǐng)求對(duì)應(yīng)的CPU占用是比較小的。
網(wǎng)游的IO主要分兩個(gè)方面,一個(gè)是網(wǎng)絡(luò)IO,一個(gè)是磁盤(pán)IO。網(wǎng)絡(luò)IO方面,可以分成美術(shù)資源的IO和游戲邏輯指令的IO,這里主要分析游戲邏輯的IO。游戲邏輯的IO跟CPU占用的情況相似,每次請(qǐng)求的字節(jié)數(shù)很小,但由于多人同時(shí)在線(xiàn),因此并發(fā)數(shù)相當(dāng)高。另外,地圖信息的廣播也會(huì)帶來(lái)比較頻繁的網(wǎng)絡(luò)通信。磁盤(pán)IO方面,主要是游戲數(shù)據(jù)的保存。采用不同的數(shù)據(jù)庫(kù),會(huì)有比較大的區(qū)別。以前的項(xiàng)目里,就經(jīng)歷了從MySQL轉(zhuǎn)向MongoDB這種內(nèi)存數(shù)據(jù)庫(kù)的過(guò)程,磁盤(pán)IO不再是瓶頸。總體來(lái)說(shuō),還是用內(nèi)存做一級(jí)緩沖,避免大量小數(shù)據(jù)塊讀寫(xiě)的方案。
針對(duì)網(wǎng)游的這些特點(diǎn),golang的語(yǔ)言特性十分適合開(kāi)發(fā)游戲服務(wù)器端。
首先,go語(yǔ)言提供goroutine機(jī)制作為原生的并發(fā)機(jī)制。每個(gè)goroutine所需的內(nèi)存很少,實(shí)際應(yīng)用中可以啟動(dòng)大量的goroutine對(duì)并發(fā)連接進(jìn)行響應(yīng)。goroutine與gevent中的greenlet很相像,遇到IO阻塞的時(shí)候,調(diào)度器就會(huì)自動(dòng)切換到另一個(gè)goroutine執(zhí)行,保證CPU不會(huì)因?yàn)镮O而發(fā)生等待。而goroutine與gevent相比,沒(méi)有了python底層的GIL限制,就不需要利用多進(jìn)程來(lái)榨取多核機(jī)器的性能了。通過(guò)設(shè)置最大線(xiàn)程數(shù),可以控制go所啟動(dòng)的線(xiàn)程,每個(gè)線(xiàn)程執(zhí)行一個(gè)goroutine,讓CPU滿(mǎn)負(fù)載運(yùn)行。
同時(shí),go語(yǔ)言為goroutine提供了獨(dú)到的通信機(jī)制channel。channel發(fā)生讀寫(xiě)的時(shí)候,也會(huì)掛起當(dāng)前操作channel的goroutine,是一種同步阻塞通信。這樣既達(dá)到了通信的目的,又實(shí)現(xiàn)同步,用CSP模型的觀點(diǎn)看,并發(fā)模型就是通過(guò)一組進(jìn)程和進(jìn)程間的事件觸發(fā)解決任務(wù)的。雖然說(shuō),主流的編程語(yǔ)言之間,只要是圖靈完備的,他們就都能實(shí)現(xiàn)相同的功能。但go語(yǔ)言提供的這種協(xié)程間通信機(jī)制,十分優(yōu)雅地揭示了協(xié)程通信的本質(zhì),避免了以往鎖的顯式使用帶給程序員的心理負(fù)擔(dān),確是一大優(yōu)勢(shì)。進(jìn)行網(wǎng)游開(kāi)發(fā)的程序員,可以將游戲邏輯按照單線(xiàn)程阻塞式的寫(xiě),不需要額外考慮線(xiàn)程調(diào)度的問(wèn)題,以及線(xiàn)程間數(shù)據(jù)依賴(lài)的問(wèn)題。因?yàn)椋€(xiàn)程間的channel通信,已經(jīng)表達(dá)了線(xiàn)程間的數(shù)據(jù)依賴(lài)關(guān)系了,而go的調(diào)度器會(huì)給予妥善的處理。
另外,go語(yǔ)言提供的gc機(jī)制,以及對(duì)指針的保護(hù)式使用,可以大大減輕程序員的開(kāi)發(fā)壓力,提高開(kāi)發(fā)效率。
展望未來(lái),我期待go語(yǔ)言社區(qū)能夠提供更多的goroutine間的隔離機(jī)制。個(gè)人十分推崇erlang社區(qū)的脆崩哲學(xué),推動(dòng)應(yīng)用發(fā)生預(yù)期外行為時(shí),盡早崩潰,再fork出新進(jìn)程處理新的請(qǐng)求。對(duì)于協(xié)程機(jī)制,需要由程序員保證執(zhí)行的函數(shù)不會(huì)發(fā)生死循環(huán),導(dǎo)致線(xiàn)程卡死。如果能夠定制goroutine所執(zhí)行函數(shù)的最大CPU執(zhí)行時(shí)間,及所能使用的最大內(nèi)存空間,對(duì)于提升系統(tǒng)的魯棒性,大有裨益。
本文目錄如下,閱讀本文后,將一網(wǎng)打盡下面Golang Map相關(guān)面試題
Go中的map是一個(gè)指針,占用8個(gè)字節(jié),指向hmap結(jié)構(gòu)體; 源碼 src/runtime/map.go 中可以看到map的底層結(jié)構(gòu)
每個(gè)map的底層結(jié)構(gòu)是hmap,hmap包含若干個(gè)結(jié)構(gòu)為bmap的bucket數(shù)組。每個(gè)bucket底層都采用鏈表結(jié)構(gòu)。接下來(lái),我們來(lái)詳細(xì)看下map的結(jié)構(gòu)
bmap 就是我們常說(shuō)的“桶”,一個(gè)桶里面會(huì)最多裝 8 個(gè) key,這些 key 之所以會(huì)落入同一個(gè)桶,是因?yàn)樗鼈兘?jīng)過(guò)哈希計(jì)算后,哈希結(jié)果是“一類(lèi)”的,關(guān)于key的定位我們?cè)趍ap的查詢(xún)和插入中詳細(xì)說(shuō)明。在桶內(nèi),又會(huì)根據(jù) key 計(jì)算出來(lái)的 hash 值的高 8 位來(lái)決定 key 到底落入桶內(nèi)的哪個(gè)位置(一個(gè)桶內(nèi)最多有8個(gè)位置)。
bucket內(nèi)存數(shù)據(jù)結(jié)構(gòu)可視化如下:
注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/... 這樣的形式。源碼里說(shuō)明這樣的好處是在某些情況下可以省略掉 padding字段,節(jié)省內(nèi)存空間。
當(dāng) map 的 key 和 value 都不是指針,并且 size 都小于 128 字節(jié)的情況下,會(huì)把 bmap 標(biāo)記為不含指針,這樣可以避免 gc 時(shí)掃描整個(gè) hmap。但是,我們看 bmap 其實(shí)有一個(gè) overflow 的字段,是指針類(lèi)型的,破壞了 bmap 不含指針的設(shè)想,這時(shí)會(huì)把 overflow 移動(dòng)到 extra 字段來(lái)。
map是個(gè)指針,底層指向hmap,所以是個(gè)引用類(lèi)型
golang 有三個(gè)常用的高級(jí)類(lèi)型 slice 、map、channel, 它們都是 引用類(lèi)型 ,當(dāng)引用類(lèi)型作為函數(shù)參數(shù)時(shí),可能會(huì)修改原內(nèi)容數(shù)據(jù)。
golang 中沒(méi)有引用傳遞,只有值和指針傳遞。所以 map 作為函數(shù)實(shí)參傳遞時(shí)本質(zhì)上也是值傳遞,只不過(guò)因?yàn)?map 底層數(shù)據(jù)結(jié)構(gòu)是通過(guò)指針指向?qū)嶋H的元素存儲(chǔ)空間,在被調(diào)函數(shù)中修改 map,對(duì)調(diào)用者同樣可見(jiàn),所以 map 作為函數(shù)實(shí)參傳遞時(shí)表現(xiàn)出了引用傳遞的效果。
因此,傳遞 map 時(shí),如果想修改map的內(nèi)容而不是map本身,函數(shù)形參無(wú)需使用指針
map 底層數(shù)據(jù)結(jié)構(gòu)是通過(guò)指針指向?qū)嶋H的元素 存儲(chǔ)空間 ,這種情況下,對(duì)其中一個(gè)map的更改,會(huì)影響到其他map
map 在沒(méi)有被修改的情況下,使用 range 多次遍歷 map 時(shí)輸出的 key 和 value 的順序可能不同。這是 Go 語(yǔ)言的設(shè)計(jì)者們有意為之,在每次 range 時(shí)的順序被隨機(jī)化,旨在提示開(kāi)發(fā)者們,Go 底層實(shí)現(xiàn)并不保證 map 遍歷順序穩(wěn)定,請(qǐng)大家不要依賴(lài) range 遍歷結(jié)果順序。
map 本身是無(wú)序的,且遍歷時(shí)順序還會(huì)被隨機(jī)化,如果想順序遍歷 map,需要對(duì) map key 先排序,再按照 key 的順序遍歷 map。
map默認(rèn)是并發(fā)不安全的,原因如下:
Go 官方在經(jīng)過(guò)了長(zhǎng)時(shí)間的討論后,認(rèn)為 Go map 更應(yīng)適配典型使用場(chǎng)景(不需要從多個(gè) goroutine 中進(jìn)行安全訪(fǎng)問(wèn)),而不是為了小部分情況(并發(fā)訪(fǎng)問(wèn)),導(dǎo)致大部分程序付出加鎖代價(jià)(性能),決定了不支持。
場(chǎng)景: 2個(gè)協(xié)程同時(shí)讀和寫(xiě),以下程序會(huì)出現(xiàn)致命錯(cuò)誤:fatal error: concurrent map writes
如果想實(shí)現(xiàn)map線(xiàn)程安全,有兩種方式:
方式一:使用讀寫(xiě)鎖 map + sync.RWMutex
方式二:使用golang提供的 sync.Map
sync.map是用讀寫(xiě)分離實(shí)現(xiàn)的,其思想是空間換時(shí)間。和map+RWLock的實(shí)現(xiàn)方式相比,它做了一些優(yōu)化:可以無(wú)鎖訪(fǎng)問(wèn)read map,而且會(huì)優(yōu)先操作read map,倘若只操作read map就可以滿(mǎn)足要求(增刪改查遍歷),那就不用去操作write map(它的讀寫(xiě)都要加鎖),所以在某些特定場(chǎng)景中它發(fā)生鎖競(jìng)爭(zhēng)的頻率會(huì)遠(yuǎn)遠(yuǎn)小于map+RWLock的實(shí)現(xiàn)方式。
golang中map是一個(gè)kv對(duì)集合。底層使用hash table,用鏈表來(lái)解決沖突 ,出現(xiàn)沖突時(shí),不是每一個(gè)key都申請(qǐng)一個(gè)結(jié)構(gòu)通過(guò)鏈表串起來(lái),而是以bmap為最小粒度掛載,一個(gè)bmap可以放8個(gè)kv。在哈希函數(shù)的選擇上,會(huì)在程序啟動(dòng)時(shí),檢測(cè) cpu 是否支持 aes,如果支持,則使用 aes hash,否則使用 memhash。
map有3鐘初始化方式,一般通過(guò)make方式創(chuàng)建
map的創(chuàng)建通過(guò)生成匯編碼可以知道,make創(chuàng)建map時(shí)調(diào)用的底層函數(shù)是 runtime.makemap 。如果你的map初始容量小于等于8會(huì)發(fā)現(xiàn)走的是 runtime.fastrand 是因?yàn)槿萘啃∮?時(shí)不需要生成多個(gè)桶,一個(gè)桶的容量就可以滿(mǎn)足
makemap函數(shù)會(huì)通過(guò) fastrand 創(chuàng)建一個(gè)隨機(jī)的哈希種子,然后根據(jù)傳入的 hint 計(jì)算出需要的最小需要的桶的數(shù)量,最后再使用 makeBucketArray 創(chuàng)建用于保存桶的數(shù)組,這個(gè)方法其實(shí)就是根據(jù)傳入的 B 計(jì)算出的需要?jiǎng)?chuàng)建的桶數(shù)量在內(nèi)存中分配一片連續(xù)的空間用于存儲(chǔ)數(shù)據(jù),在創(chuàng)建桶的過(guò)程中還會(huì)額外創(chuàng)建一些用于保存溢出數(shù)據(jù)的桶,數(shù)量是 2^(B-4) 個(gè)。初始化完成返回hmap指針。
找到一個(gè) B,使得 map 的裝載因子在正常范圍內(nèi)
Go 語(yǔ)言中讀取 map 有兩種語(yǔ)法:帶 comma 和 不帶 comma。當(dāng)要查詢(xún)的 key 不在 map 里,帶 comma 的用法會(huì)返回一個(gè) bool 型變量提示 key 是否在 map 中;而不帶 comma 的語(yǔ)句則會(huì)返回一個(gè) value 類(lèi)型的零值。如果 value 是 int 型就會(huì)返回 0,如果 value 是 string 類(lèi)型,就會(huì)返回空字符串。
map的查找通過(guò)生成匯編碼可以知道,根據(jù) key 的不同類(lèi)型,編譯器會(huì)將查找函數(shù)用更具體的函數(shù)替換,以?xún)?yōu)化效率:
函數(shù)首先會(huì)檢查 map 的標(biāo)志位 flags。如果 flags 的寫(xiě)標(biāo)志位此時(shí)被置 1 了,說(shuō)明有其他協(xié)程在執(zhí)行“寫(xiě)”操作,進(jìn)而導(dǎo)致程序 panic。這也說(shuō)明了 map 對(duì)協(xié)程是不安全的。
key經(jīng)過(guò)哈希函數(shù)計(jì)算后,得到的哈希值如下(主流64位機(jī)下共 64 個(gè) bit 位):
m: 桶的個(gè)數(shù)
從buckets 通過(guò) hash m 得到對(duì)應(yīng)的bucket,如果bucket正在擴(kuò)容,并且沒(méi)有擴(kuò)容完成,則從oldbuckets得到對(duì)應(yīng)的bucket
計(jì)算hash所在桶編號(hào):
用上一步哈希值最后的 5 個(gè) bit 位,也就是 01010 ,值為 10,也就是 10 號(hào)桶(范圍是0~31號(hào)桶)
計(jì)算hash所在的槽位:
用上一步哈希值哈希值的高8個(gè)bit 位,也就是 10010111 ,轉(zhuǎn)化為十進(jìn)制,也就是151,在 10 號(hào) bucket 中尋找** tophash 值(HOB hash)為 151* 的 槽位**,即為key所在位置,找到了 2 號(hào)槽位,這樣整個(gè)查找過(guò)程就結(jié)束了。
如果在 bucket 中沒(méi)找到,并且 overflow 不為空,還要繼續(xù)去 overflow bucket 中尋找,直到找到或是所有的 key 槽位都找遍了,包括所有的 overflow bucket。
通過(guò)上面找到了對(duì)應(yīng)的槽位,這里我們?cè)僭敿?xì)分析下key/value值是如何獲取的:
bucket 里 key 的起始地址就是 unsafe.Pointer(b)+dataOffset。第 i 個(gè) key 的地址就要在此基礎(chǔ)上跨過(guò) i 個(gè) key 的大小;而我們又知道,value 的地址是在所有 key 之后,因此第 i 個(gè) value 的地址還需要加上所有 key 的偏移。
通過(guò)匯編語(yǔ)言可以看到,向 map 中插入或者修改 key,最終調(diào)用的是 mapassign 函數(shù)。
實(shí)際上插入或修改 key 的語(yǔ)法是一樣的,只不過(guò)前者操作的 key 在 map 中不存在,而后者操作的 key 存在 map 中。
mapassign 有一個(gè)系列的函數(shù),根據(jù) key 類(lèi)型的不同,編譯器會(huì)將其優(yōu)化為相應(yīng)的“快速函數(shù)”。
我們只用研究最一般的賦值函數(shù) mapassign 。
map的賦值會(huì)附帶著map的擴(kuò)容和遷移,map的擴(kuò)容只是將底層數(shù)組擴(kuò)大了一倍,并沒(méi)有進(jìn)行數(shù)據(jù)的轉(zhuǎn)移,數(shù)據(jù)的轉(zhuǎn)移是在擴(kuò)容后逐步進(jìn)行的,在遷移的過(guò)程中每進(jìn)行一次賦值(access或者delete)會(huì)至少做一次遷移工作。
1.判斷map是否為nil
每一次進(jìn)行賦值/刪除操作時(shí),只要oldbuckets != nil 則認(rèn)為正在擴(kuò)容,會(huì)做一次遷移工作,下面會(huì)詳細(xì)說(shuō)下遷移過(guò)程
根據(jù)上面查找過(guò)程,查找key所在位置,如果找到則更新,沒(méi)找到則找空位插入即可
經(jīng)過(guò)前面迭代尋找動(dòng)作,若沒(méi)有找到可插入的位置,意味著需要擴(kuò)容進(jìn)行插入,下面會(huì)詳細(xì)說(shuō)下擴(kuò)容過(guò)程
通過(guò)匯編語(yǔ)言可以看到,向 map 中刪除 key,最終調(diào)用的是 mapdelete 函數(shù)
刪除的邏輯相對(duì)比較簡(jiǎn)單,大多函數(shù)在賦值操作中已經(jīng)用到過(guò),核心還是找到 key 的具體位置。尋找過(guò)程都是類(lèi)似的,在 bucket 中挨個(gè) cell 尋找。找到對(duì)應(yīng)位置后,對(duì) key 或者 value 進(jìn)行“清零”操作,將 count 值減 1,將對(duì)應(yīng)位置的 tophash 值置成 Empty
再來(lái)說(shuō)觸發(fā) map 擴(kuò)容的時(shí)機(jī):在向 map 插入新 key 的時(shí)候,會(huì)進(jìn)行條件檢測(cè),符合下面這 2 個(gè)條件,就會(huì)觸發(fā)擴(kuò)容:
1、裝載因子超過(guò)閾值
源碼里定義的閾值是 6.5 (loadFactorNum/loadFactorDen),是經(jīng)過(guò)測(cè)試后取出的一個(gè)比較合理的因子
我們知道,每個(gè) bucket 有 8 個(gè)空位,在沒(méi)有溢出,且所有的桶都裝滿(mǎn)了的情況下,裝載因子算出來(lái)的結(jié)果是 8。因此當(dāng)裝載因子超過(guò) 6.5 時(shí),表明很多 bucket 都快要裝滿(mǎn)了,查找效率和插入效率都變低了。在這個(gè)時(shí)候進(jìn)行擴(kuò)容是有必要的。
對(duì)于條件 1,元素太多,而 bucket 數(shù)量太少,很簡(jiǎn)單:將 B 加 1,bucket 最大數(shù)量( 2^B )直接變成原來(lái) bucket 數(shù)量的 2 倍。于是,就有新老 bucket 了。注意,這時(shí)候元素都在老 bucket 里,還沒(méi)遷移到新的 bucket 來(lái)。新 bucket 只是最大數(shù)量變?yōu)樵瓉?lái)最大數(shù)量的 2 倍( 2^B * 2 ) 。
2、overflow 的 bucket 數(shù)量過(guò)多
在裝載因子比較小的情況下,這時(shí)候 map 的查找和插入效率也很低,而第 1 點(diǎn)識(shí)別不出來(lái)這種情況。表面現(xiàn)象就是計(jì)算裝載因子的分子比較小,即 map 里元素總數(shù)少,但是 bucket 數(shù)量多(真實(shí)分配的 bucket 數(shù)量多,包括大量的 overflow bucket)
不難想像造成這種情況的原因:不停地插入、刪除元素。先插入很多元素,導(dǎo)致創(chuàng)建了很多 bucket,但是裝載因子達(dá)不到第 1 點(diǎn)的臨界值,未觸發(fā)擴(kuò)容來(lái)緩解這種情況。之后,刪除元素降低元素總數(shù)量,再插入很多元素,導(dǎo)致創(chuàng)建很多的 overflow bucket,但就是不會(huì)觸發(fā)第 1 點(diǎn)的規(guī)定,你能拿我怎么辦?overflow bucket 數(shù)量太多,導(dǎo)致 key 會(huì)很分散,查找插入效率低得嚇人,因此出臺(tái)第 2 點(diǎn)規(guī)定。這就像是一座空城,房子很多,但是住戶(hù)很少,都分散了,找起人來(lái)很困難
對(duì)于條件 2,其實(shí)元素沒(méi)那么多,但是 overflow bucket 數(shù)特別多,說(shuō)明很多 bucket 都沒(méi)裝滿(mǎn)。解決辦法就是開(kāi)辟一個(gè)新 bucket 空間,將老 bucket 中的元素移動(dòng)到新 bucket,使得同一個(gè) bucket 中的 key 排列地更緊密。這樣,原來(lái),在 overflow bucket 中的 key 可以移動(dòng)到 bucket 中來(lái)。結(jié)果是節(jié)省空間,提高 bucket 利用率,map 的查找和插入效率自然就會(huì)提升。
由于 map 擴(kuò)容需要將原有的 key/value 重新搬遷到新的內(nèi)存地址,如果有大量的 key/value 需要搬遷,會(huì)非常影響性能。因此 Go map 的擴(kuò)容采取了一種稱(chēng)為“漸進(jìn)式”的方式,原有的 key 并不會(huì)一次性搬遷完畢,每次最多只會(huì)搬遷 2 個(gè) bucket。
上面說(shuō)的 hashGrow() 函數(shù)實(shí)際上并沒(méi)有真正地“搬遷”,它只是分配好了新的 buckets,并將老的 buckets 掛到了 oldbuckets 字段上。真正搬遷 buckets 的動(dòng)作在 growWork() 函數(shù)中,而調(diào)用 growWork() 函數(shù)的動(dòng)作是在 mapassign 和 mapdelete 函數(shù)中。也就是插入或修改、刪除 key 的時(shí)候,都會(huì)嘗試進(jìn)行搬遷 buckets 的工作。先檢查 oldbuckets 是否搬遷完畢,具體來(lái)說(shuō)就是檢查 oldbuckets 是否為 nil。
如果未遷移完畢,賦值/刪除的時(shí)候,擴(kuò)容完畢后(預(yù)分配內(nèi)存),不會(huì)馬上就進(jìn)行遷移。而是采取 增量擴(kuò)容 的方式,當(dāng)有訪(fǎng)問(wèn)到具體 bukcet 時(shí),才會(huì)逐漸的進(jìn)行遷移(將 oldbucket 遷移到 bucket)
nevacuate 標(biāo)識(shí)的是當(dāng)前的進(jìn)度,如果都搬遷完,應(yīng)該和2^B的長(zhǎng)度是一樣的
在evacuate 方法實(shí)現(xiàn)是把這個(gè)位置對(duì)應(yīng)的bucket,以及其沖突鏈上的數(shù)據(jù)都轉(zhuǎn)移到新的buckets上。
轉(zhuǎn)移的判斷直接通過(guò)tophash 就可以,判斷tophash中第一個(gè)hash值即可
遍歷的過(guò)程,就是按順序遍歷 bucket,同時(shí)按順序遍歷 bucket 中的 key。
map遍歷是無(wú)序的,如果想實(shí)現(xiàn)有序遍歷,可以先對(duì)key進(jìn)行排序
為什么遍歷 map 是無(wú)序的?
如果發(fā)生過(guò)遷移,key 的位置發(fā)生了重大的變化,有些 key 飛上高枝,有些 key 則原地不動(dòng)。這樣,遍歷 map 的結(jié)果就不可能按原來(lái)的順序了。
如果就一個(gè)寫(xiě)死的 map,不會(huì)向 map 進(jìn)行插入刪除的操作,按理說(shuō)每次遍歷這樣的 map 都會(huì)返回一個(gè)固定順序的 key/value 序列吧。但是 Go 杜絕了這種做法,因?yàn)檫@樣會(huì)給新手程序員帶來(lái)誤解,以為這是一定會(huì)發(fā)生的事情,在某些情況下,可能會(huì)釀成大錯(cuò)。
Go 做得更絕,當(dāng)我們?cè)诒闅v map 時(shí),并不是固定地從 0 號(hào) bucket 開(kāi)始遍歷,每次都是從一個(gè)**隨機(jī)值序號(hào)的 bucket 開(kāi)始遍歷,并且是從這個(gè) bucket 的一個(gè) 隨機(jī)序號(hào)的 cell **開(kāi)始遍歷。這樣,即使你是一個(gè)寫(xiě)死的 map,僅僅只是遍歷它,也不太可能會(huì)返回一個(gè)固定序列的 key/value 對(duì)了。
很多朋友可能知道Go語(yǔ)言的優(yōu)勢(shì)在哪,卻不知道Go語(yǔ)言適合用于哪些地方。
1、 Go語(yǔ)言作為服務(wù)器編程語(yǔ)言,很適合處理日志、數(shù)據(jù)打包、虛擬機(jī)處理、文件系統(tǒng)、分布式系統(tǒng)、數(shù)據(jù)庫(kù)代理等;網(wǎng)絡(luò)編程方面。Go語(yǔ)言廣泛應(yīng)用于Web應(yīng)用、API應(yīng)用、下載應(yīng)用等;除此之外,Go語(yǔ)言還可用于內(nèi)存數(shù)據(jù)庫(kù)和云平臺(tái)領(lǐng)域,目前國(guó)外很多云平臺(tái)都是采用Go開(kāi)發(fā)。
2、 其實(shí)Go語(yǔ)言主要用作服務(wù)器端開(kāi)發(fā)。其定位是用來(lái)開(kāi)發(fā)"大型軟件"的,適合于很多程序員一起開(kāi)發(fā)大型軟件,并且開(kāi)發(fā)周期長(zhǎng),支持云計(jì)算的網(wǎng)絡(luò)服務(wù)。Go語(yǔ)言能夠讓程序員快速開(kāi)發(fā),并且在軟件不斷的增長(zhǎng)過(guò)程中,它能讓程序員更容易地進(jìn)行維護(hù)和修改。它融合了傳統(tǒng)編譯型語(yǔ)言的高效性和腳本語(yǔ)言的易用性和富于表達(dá)性。
3、 Go語(yǔ)言成功案例。Nsq:Nsq是由Go語(yǔ)言開(kāi)發(fā)的高性能、高可用消息隊(duì)列系統(tǒng),性能非常高,每天能處理數(shù)十億條的消息;
4、 Docker:基于lxc的一個(gè)虛擬打包工具,能夠?qū)崿F(xiàn)PAAS平臺(tái)的組建。
5、 Packer:用來(lái)生成不同平臺(tái)的鏡像文件,例如VM、vbox、AWS等,作者是vagrant的作者
6、 Skynet:分布式調(diào)度框架。
7、 Doozer:分布式同步工具,類(lèi)似ZooKeeper。
8、 Heka:mazila開(kāi)源的日志處理系統(tǒng)。
9、 Cbfs:couchbase開(kāi)源的分布式文件系統(tǒng)。
10、 Tsuru:開(kāi)源的PAAS平臺(tái),和SAE實(shí)現(xiàn)的功能一模一樣。
11、 Groupcache:memcahe作者寫(xiě)的用于Google下載系統(tǒng)的緩存系統(tǒng)。
12、 God:類(lèi)似redis的緩存系統(tǒng),但是支持分布式和擴(kuò)展性。
13、 Gor:網(wǎng)絡(luò)流量抓包和重放工具。
以上的就是關(guān)于go語(yǔ)言能做什么的內(nèi)容介紹了。
本文題目:go語(yǔ)言地圖服務(wù)器 google地圖
文章出自:http://www.yijiale78.com/article12/hpdcdc.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供移動(dòng)網(wǎng)站建設(shè)、網(wǎng)站營(yíng)銷(xiāo)、網(wǎng)站設(shè)計(jì)、搜索引擎優(yōu)化、品牌網(wǎng)站建設(shè)、響應(yīng)式網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話(huà):028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)