參考:
網站建設哪家好,找成都創(chuàng)新互聯(lián)公司!專注于網頁設計、網站建設、微信開發(fā)、成都微信小程序、集團企業(yè)網站建設等服務項目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了開原免費建站歡迎大家使用!
Goroutine并發(fā)調度模型深度解析手擼一個協(xié)程池
Golang 的 goroutine 是如何實現(xiàn)的?
Golang - 調度剖析【第二部分】
OS線程初始棧為2MB。Go語言中,每個goroutine采用動態(tài)擴容方式,初始2KB,按需增長,最大1G。此外GC會收縮棧空間。
BTW,增長擴容都是有代價的,需要copy數(shù)據(jù)到新的stack,所以初始2KB可能有些性能問題。
更多關于stack的內容,可以參見大佬的文章。 聊一聊goroutine stack
用戶線程的調度以及生命周期管理都是用戶層面,Go語言自己實現(xiàn)的,不借助OS系統(tǒng)調用,減少系統(tǒng)資源消耗。
Go語言采用兩級線程模型,即用戶線程與內核線程KSE(kernel scheduling entity)是M:N的。最終goroutine還是會交給OS線程執(zhí)行,但是需要一個中介,提供上下文。這就是G-M-P模型
Go調度器有兩個不同的運行隊列:
go1.10\src\runtime\runtime2.go
Go調度器根據(jù)事件進行上下文切換。
調度的目的就是防止M堵塞,空閑,系統(tǒng)進程切換。
詳見 Golang - 調度剖析【第二部分】
Linux可以通過epoll實現(xiàn)網絡調用,統(tǒng)稱網絡輪詢器N(Net Poller)。
文件IO操作
上面都是防止M堵塞,任務竊取是防止M空閑
每個M都有一個特殊的G,g0。用于執(zhí)行調度,gc,棧管理等任務,所以g0的棧稱為調度棧。g0的棧不會自動增長,不會被gc,來自os線程的棧。
go1.10\src\runtime\proc.go
G沒辦法自己運行,必須通過M運行
M通過通過調度,執(zhí)行G
從M掛載P的runq中找到G,執(zhí)行G
前段時間在golang-China讀到這個貼:
個人覺得golang十分適合進行網游服務器端開發(fā),寫下這篇文章總結一下。
從網游的角度看:
要成功的運營一款網游,很大程度上依賴于玩家自發(fā)形成的社區(qū)。只有玩家自發(fā)形成一個穩(wěn)定的生態(tài)系統(tǒng),游戲才能持續(xù)下去,避免鬼城的出現(xiàn)。而這就需要多次大量導入用戶,在同時在線用戶量達到某個臨界點的時候,才有可能完成。因此,多人同時在線十分有必要。
再來看網游的常見玩法,除了排行榜這類統(tǒng)計和數(shù)據(jù)匯總的功能外,基本沒有需要大量CPU時間的應用。以前的項目里,即時戰(zhàn)斗產生的各種傷害計算對CPU的消耗也不大。玩家要完成一次操作,需要通過客戶端-服務器端-客戶端這樣一個來回,為了獲得高響應速度,滿足玩家體驗,服務器端的處理也不能占用太多時間。所以,每次請求對應的CPU占用是比較小的。
網游的IO主要分兩個方面,一個是網絡IO,一個是磁盤IO。網絡IO方面,可以分成美術資源的IO和游戲邏輯指令的IO,這里主要分析游戲邏輯的IO。游戲邏輯的IO跟CPU占用的情況相似,每次請求的字節(jié)數(shù)很小,但由于多人同時在線,因此并發(fā)數(shù)相當高。另外,地圖信息的廣播也會帶來比較頻繁的網絡通信。磁盤IO方面,主要是游戲數(shù)據(jù)的保存。采用不同的數(shù)據(jù)庫,會有比較大的區(qū)別。以前的項目里,就經歷了從MySQL轉向MongoDB這種內存數(shù)據(jù)庫的過程,磁盤IO不再是瓶頸??傮w來說,還是用內存做一級緩沖,避免大量小數(shù)據(jù)塊讀寫的方案。
針對網游的這些特點,golang的語言特性十分適合開發(fā)游戲服務器端。
首先,go語言提供goroutine機制作為原生的并發(fā)機制。每個goroutine所需的內存很少,實際應用中可以啟動大量的goroutine對并發(fā)連接進行響應。goroutine與gevent中的greenlet很相像,遇到IO阻塞的時候,調度器就會自動切換到另一個goroutine執(zhí)行,保證CPU不會因為IO而發(fā)生等待。而goroutine與gevent相比,沒有了python底層的GIL限制,就不需要利用多進程來榨取多核機器的性能了。通過設置最大線程數(shù),可以控制go所啟動的線程,每個線程執(zhí)行一個goroutine,讓CPU滿負載運行。
同時,go語言為goroutine提供了獨到的通信機制channel。channel發(fā)生讀寫的時候,也會掛起當前操作channel的goroutine,是一種同步阻塞通信。這樣既達到了通信的目的,又實現(xiàn)同步,用CSP模型的觀點看,并發(fā)模型就是通過一組進程和進程間的事件觸發(fā)解決任務的。雖然說,主流的編程語言之間,只要是圖靈完備的,他們就都能實現(xiàn)相同的功能。但go語言提供的這種協(xié)程間通信機制,十分優(yōu)雅地揭示了協(xié)程通信的本質,避免了以往鎖的顯式使用帶給程序員的心理負擔,確是一大優(yōu)勢。進行網游開發(fā)的程序員,可以將游戲邏輯按照單線程阻塞式的寫,不需要額外考慮線程調度的問題,以及線程間數(shù)據(jù)依賴的問題。因為,線程間的channel通信,已經表達了線程間的數(shù)據(jù)依賴關系了,而go的調度器會給予妥善的處理。
另外,go語言提供的gc機制,以及對指針的保護式使用,可以大大減輕程序員的開發(fā)壓力,提高開發(fā)效率。
展望未來,我期待go語言社區(qū)能夠提供更多的goroutine間的隔離機制。個人十分推崇erlang社區(qū)的脆崩哲學,推動應用發(fā)生預期外行為時,盡早崩潰,再fork出新進程處理新的請求。對于協(xié)程機制,需要由程序員保證執(zhí)行的函數(shù)不會發(fā)生死循環(huán),導致線程卡死。如果能夠定制goroutine所執(zhí)行函數(shù)的最大CPU執(zhí)行時間,及所能使用的最大內存空間,對于提升系統(tǒng)的魯棒性,大有裨益。
在go http每一次go serve(l)都會構建Request數(shù)據(jù)結構。在大量數(shù)據(jù)請求或高并發(fā)的場景中,頻繁創(chuàng)建銷毀對象,會導致GC壓力。解決辦法之一就是使用對象復用技術。在http協(xié)議層之下,使用對象復用技術創(chuàng)建Request數(shù)據(jù)結構。在http協(xié)議層之上,可以使用對象復用技術創(chuàng)建(w,*r,ctx)數(shù)據(jù)結構。這樣即可以回快TCP層讀包之后的解析速度,也可也加快請求處理的速度。
先上一個測試:
結論是這樣的:
貌似使用池化,性能弱爆了???這似乎與net/http使用sync.pool池化Request來優(yōu)化性能的選擇相違背。這同時也說明了一個問題,好的東西,如果濫用反而造成了性能成倍的下降。在看過pool原理之后,結合實例,將給出正確的使用方法,并給出預期的效果。
sync.Pool是一個 協(xié)程安全 的 臨時對象池 。數(shù)據(jù)結構如下:
local 成員的真實類型是一個 poolLocal 數(shù)組,localSize 是數(shù)組長度。這涉及到Pool實現(xiàn),pool為每個P分配了一個對象,P數(shù)量設置為runtime.GOMAXPROCS(0)。在并發(fā)讀寫時,goroutine綁定的P有對象,先用自己的,沒有去偷其它P的。go語言將數(shù)據(jù)分散在了各個真正運行的P中,降低了鎖競爭,提高了并發(fā)能力。
不要習慣性地誤認為New是一個關鍵字,這里的New是Pool的一個字段,也是一個閉包名稱。其API:
如果不指定New字段,對象池為空時會返回nil,而不是一個新構建的對象。Get()到的對象是隨機的。
原生sync.Pool的問題是,Pool中的對象會被GC清理掉,這使得sync.Pool只適合做簡單地對象池,不適合作連接池。
pool創(chuàng)建時不能指定大小,沒有數(shù)量限制。pool中對象會被GC清掉,只存在于兩次GC之間。實現(xiàn)是pool的init方法注冊了一個poolCleanup()函數(shù),這個方法在GC之前執(zhí)行,清空pool中的所有緩存對象。
為使多協(xié)程使用同一個POOL。最基本的想法就是每個協(xié)程,加鎖去操作共享的POOL,這顯然是低效的。而進一步改進,類似于ConcurrentHashMap(JDK7)的分Segment,提高其并發(fā)性可以一定程度性緩解。
注意到pool中的對象是無差異性的,加鎖或者分段加鎖都不是較好的做法。go的做法是為每一個綁定協(xié)程的P都分配一個子池。每個子池又分為私有池和共享列表。共享列表是分別存放在各個P之上的共享區(qū)域,而不是各個P共享的一塊內存。協(xié)程拿自己P里的子池對象不需要加鎖,拿共享列表中的就需要加鎖了。
Get對象過程:
Put過程:
如何解決Get最壞情況遍歷所有P才獲取得對象呢:
方法1止前sync.pool并沒有這樣的設置。方法2由于goroutine被分配到哪個P由調度器調度不可控,無法確保其平衡。
由于不可控的GC導致生命周期過短,且池大小不可控,因而不適合作連接池。僅適用于增加對象重用機率,減少GC負擔。2
執(zhí)行結果:
單線程情況下,遍歷其它無元素的P,長時間加鎖性能低下。啟用協(xié)程改善。
結果:
測試場景在goroutines遠大于GOMAXPROCS情況下,與非池化性能差異巨大。
測試結果
可以看到同樣使用*sync.pool,較大池大小的命中率較高,性能遠高于空池。
結論:pool在一定的使用條件下提高并發(fā)性能,條件1是協(xié)程數(shù)遠大于GOMAXPROCS,條件2是池中對象遠大于GOMAXPROCS。歸結成一個原因就是使對象在各個P中均勻分布。
池pool和緩存cache的區(qū)別。池的意思是,池內對象是可以互換的,不關心具體值,甚至不需要區(qū)分是新建的還是從池中拿出的。緩存指的是KV映射,緩存里的值互不相同,清除機制更為復雜。緩存清除算法如LRU、LIRS緩存算法。
池空間回收的幾種方式。一些是GC前回收,一些是基于時鐘或弱引用回收。最終確定在GC時回收Pool內對象,即不回避GC。用java的GC解釋弱引用。GC的四種引用:強引用、弱引用、軟引用、虛引用。虛引用即沒有引用,弱引用GC但有空間則保留,軟引用GC即清除。ThreadLocal的值為弱引用的例子。
regexp 包為了保證并發(fā)時使用同一個正則,而維護了一組狀態(tài)機。
fmt包做字串拼接,從sync.pool拿[]byte對象。避免頻繁構建再GC效率高很多。
分享名稱:go語言并發(fā)上限 go實現(xiàn)并發(fā)
標題路徑:http://www.rwnh.cn/article26/ddcpicg.html
成都網站建設公司_創(chuàng)新互聯(lián),為您提供微信小程序、網站制作、網站內鏈、建站公司、標簽優(yōu)化、面包屑導航
聲明:本網站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)