golang實踐-非同步系統的無鎖

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

背景

這段時間,重構了一些服務的基礎工具庫,主要是解耦pub-sub改為非同步系統[eventbus],簡單調整了定時器[clock]。本來以為已經大幅簡化了業務沒問題了,結果5月份,其中一個服務因為廣播事件,導致死結。分析後,發現是一個非常基礎的問題導致,值得捋一捋。

問題原因大致是這樣:

有一個服務物件,通過RPC,對外提供多個公用服務,並可以反向推送訊息給用戶端。其中,

  1. 有多個rpc方法,被用戶端調用,有的方法需要資料保護,調用了鎖。
  2. 該服務能收到服務端的網路斷開命令,會調用名為ServerNetReset方法能夠隨時重設網路代理程式這個私人屬性,也調用了鎖。
  3. 該服務物件訂閱了一些事件,一旦觸發就會向用戶端主動調用Notify方法來推送訊息。為了避免該操作時,網路代理程式被ServerNetReset方法清空,因此Notify調用的私人方法push也調用了鎖。

結果某一次業務重構,有一個方法Foo在使用過程中某種情況下調用了Notify推送訊息,同時因為Foo有對象私人資料維護,直接使用了鎖。於是,就出現了死結狀態。

相信類似於這種問題的情境,還會很多,只是我們沒有發現。

最終,我們改進方案是:網路重設、訊息推送這兩種操作,通過訊息方式串列執行,不再用鎖。

使用情境

學習go的時候,很多資料都提到:“多用通道(chan),少用鎖”。對於長期習慣同步編程,方法之間直接調用,對其中的理解並不深入,很多人更多把chan作為訊號傳遞。因為非同步呼叫涉及到事件定義、訂閱發布系統、延時返回,遠遠沒有直接調用方法來的簡便。因此,一個上萬行代碼的項目,會使用大量的鎖來保護對象屬性。

如果要採用通道,不用鎖,就不得不在“開發效率”、“運行效率”、“資源佔用”這三個方面權衡。簡單來看:

基本工具庫對象,單向引用,建議用鎖。

通過鎖進行對象內部屬性的保護,同步直接調用對象提供的公用方法,是運行效率最高的。如果該對象運行樣本不需要與其他執行個體“相互關聯”,而僅僅是被引用,則用鎖是完全沒問題,也是最簡單的。

比如,我們做一個支援並發的計數器,不存在對第三方對象的引用,這時候,只需要用鎖即可。比如:

//Counter counter is a multi-thread safe counterstype Counter struct {mut     sync.MutexcurrNum int64 //當前數量maxNum  int64 //最大數量}//AddOne 在原內部計數基礎上,+1。func (c *Counter) AddOne() int {new := atomic.AddInt64(&c.currNum, 1)c.mut.Lock()if c.maxNum < new {c.maxNum = new}c.mut.Unlock()return int(c.currNum)}//DecOne 在原內部計數基礎上,-1。func (c *Counter) DecOne() int {return int(atomic.AddInt64(&c.currNum, -1))}//Current 擷取當前內部計數結果。func (c *Counter) Current() int {return int(atomic.LoadInt64(&c.currNum))}//MaxNum 計數器生存周期內,最大的計數。func (c *Counter) MaxNum() int {return int(atomic.LoadInt64(&c.maxNum))}//NewCounter counter constructorfunc NewCounter() *Counter {return &Counter{}}

業務對象,尤其是DDD中提到的聚集之間,優先考慮用訊息架構解耦。

由於對象間引用非常複雜,最容易理解的就是魔獸世界的戰鬥情境:雙方多個玩家相互配合,不斷施展攻擊、輔助技能,過程中有的英雄使用了道具,有的被攻擊導致死亡等。如果採用同步調用,對象A調用對象B,B調用C,C執行完成後,某條件下需要再告知A,那就非常複雜。這時候,我們考慮到的是ECS架構,基本的就是pub-sub系統支援。

對於pubsub的使用,不同系統介面略有不同,大家也比較熟悉,這裡就不舉例。

在複雜的業務對象建議用非同步訊息

如果一個執行個體存在多個公用方法+私人方法,類似於前面問題背景描述的那樣,既有外部UI帶來的命令驅動,又有內部的訊息架構驅動。考慮到並發,不得不引入鎖的時候,則建議採用串列非同步方式。所有業務方法不對外,對象只有建立、接受訊息、銷毀三個對外的公用方法。所有訊息只有一個入口,這樣,就可以不用鎖了。代碼結構非常簡單:

type Message1 struct {}type Message2 struct {}type A struct {close  int32            //對象是否關閉的標誌msgbuf chan interface{} //訊息緩衝}func NewA() *A {a := &A{msgbuf: make(chan interface{}, 10),}go a.receive()return a}func (a *A) Post(message interface{}) {if atomic.LoadInt32(&a.close) == 1 {a.msgbuf <- message}}func (a *A) receive() {//通過defer實現簡單的故障隔離defer func() {if err := recover(); err != nil {log.Println(err)}}()//執行訊息處理for message := range a.msgbuf {switch msg := message.(type) {case Message1:a.foo1(msg)case Message2:a.foo2(msg)}}}func (a *A) foo1(message Message1) {}func (a *A) foo2(message Message2) {}func (a *A) Close() {if atomic.CompareAndSwapInt32(&a.close, 0, 1) {//do other thing}}//...

特彆強調,即使作為Consumer,在pub-sub系統中訂閱事件,也只傳遞Post作為事件響應的函數控制代碼,這樣即使複雜系統也不會出現因為多方法執行操作內部屬性,需要加鎖保護,而帶來的負面問題。

當然,非同步訊息對象的使用需要基本功,並且有額外的工作:

  • 清楚架構。比如rpc服務的對象是一個,但其中被調用的方法是並行。原因是 rpc/server.go Line481 用了協程:
go service.call(server, sending, mtype, req, argv, replyv, codec)
  • 前期構架不如同步調用直觀,構建緩慢,效益要在維護時才能體現。
  • 有的訊息回複會很麻煩,需要Future、Promise、Callback這些模式,而go標準庫沒有原生支援。

對於Future、Promise、Callback模式,其實很簡單,本質上就是非同步架構提供的,用於業務對象間的值對象。go原生的waitgroup、cond、chan已經提供了很好的原生支援,自己很容易擴充。

此外,從理論到實踐長達40年的actor模型也非常不錯,相對於scala、java等,go的記憶體優勢非常明顯,也非常不錯。因為這兩部分涉及到更多設計模式的使用,內容不少,有時間另開帖說說。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.