以太坊之event包--發佈訂閱模式詳解

代碼:github.com/ethereum/go-ethereum
版本:96157a897be2032be5fdb87f947fbe5df8a53bd4

一、總結

event包實現了以太坊中的事件發佈訂閱模式,這個包中主要有兩個對象,一個TypeMux,一個Feed,其中TypeMux已經被標記爲Deprecated,推薦使用Feed

二、源碼分析

1、TypeMux分析 (推薦使用Feed,但是還有地方使用)

a、主要結構體

TypeMux結構體

type TypeMux struct {
   
   
   mutex   sync.RWMutex
   subm    map[reflect.Type][]*TypeMuxSubscription //保存對應的訂閱事件
   stopped bool //是否停止
}

TypeMuxSubscription結構體

type TypeMuxSubscription struct {
   
   
   mux     *TypeMux
   created time.Time //創建時間
   closeMu sync.Mutex
   closing chan struct{
   
   } //當在關閉得時候
   closed  bool

   // these two are the same channel. they are stored separately so
   // postC can be set to nil without affecting the return value of
   // Chan.
   postMu sync.RWMutex
   readC  <-chan *TypeMuxEvent
   postC  chan<- *TypeMuxEvent
}

TypeMuxEvent事件數據結構體

type TypeMuxEvent struct {
   
   
   Time time.Time
   Data interface{
   
   } //對應的事件得結構體
}

b、流程梳理

1、訂閱事件

每個事件我們都需要先調用*TypeMuxSubscribe方法進行訂閱,並返回訂閱句柄(*TypeMuxSubscription),句柄可以用Chan方法獲取到訂閱通道(readC

Subscribe方法如下:

func (mux *TypeMux) Subscribe(types ...interface{
   
   }) *TypeMuxSubscription {
   
   
   sub := newsub(mux) //這裏構造TypeMuxSubscription類型
   mux.mutex.Lock() //加鎖,防止併發問題
   defer mux.mutex.Unlock()
   if mux.stopped {
   
    //如果發佈訂閱已經停止了,就不能再添加了。
      // set the status to closed so that calling Unsubscribe after this
      // call will short circuit.
      sub.closed = true
      close(sub.postC)
   } else {
   
   
      if mux.subm == nil {
   
    //如果還沒有初始化過,先初始化,注意鍵名是reflect.Type類型
         mux.subm = make(map[reflect.Type][]*TypeMuxSubscription)
      }
      for _, t := range types {
   
   
         rtyp := reflect.TypeOf(t)
         oldsubs := mux.subm[rtyp]
         if find(oldsubs, sub) != -1 {
   
    //如果添加過,就不需要再添加了,panic
            panic(fmt.Sprintf("event: duplicate type %s in Subscribe", rtyp))
         }
         subs := make([]*TypeMuxSubscription, len(oldsubs)+1)//構造新的slice,長度爲原來的+1
         copy(subs, oldsubs) //拷貝
         subs[len(oldsubs)] = sub //賦值slice
         mux.subm[rtyp] = subs //賦值map
      }
   }
   return sub //最後返回TypeMuxSubscription類型
}

newsub方法(c是沒有緩衝得通道):

func newsub(mux *TypeMux) *TypeMuxSubscription {
   
   
   c := make(chan *TypeMuxEvent) //構造通道,沒有緩衝得通道
   return &TypeMuxSubscription{
   
     
      mux:     mux,
      created: time.Now(), //創建時間
      readC:   c, //這裏readC和postC是同一個通道,只不過一個出一個進
      postC:   c,
      closing: make(chan struct{
   
   }),
   }
}
2、發佈事件

發佈方可以調用Post方法進行事件發佈

Post方法

func (mux *TypeMux) Post(ev interface{
   
   }) error {
   
   
   event := &TypeMuxEvent{
   
    //創建內部事件
      Time: time.Now(),
      Data: ev,
   }
   rtyp := reflect.TypeOf(ev) //reflect類型
   mux.mutex.RLock() //加鎖防止併發讀寫
   if mux.stopped {
   
   //如果都已經停止了,就別再發佈了
      mux.mutex.RUnlock()
      return ErrMuxClosed
   }
   subs := mux.subm[rtyp] //從map中取出來對應的訂閱者
   mux.mutex.RUnlock()
   for _, sub := range subs {
   
   
      sub.deliver(event) //處理訂閱事件
   }
   return nil
}

從上面我們可以看到最終訂閱事件都是在deliver方法裏面進行處理

deliver方法

func (s *TypeMuxSubscription) deliver(event *TypeMuxEvent) {
   
   
   // Short circuit delivery if stale event
   if s.created.After(event.Time) {
   
    //如果訂閱句柄得創建時間比事件創建得時間還晚,明顯有問題,返回
      return
   }
   // Otherwise deliver the event
   s.postMu.RLock()
   defer s.postMu.RUnlock()

   select {
   
   
   case s.postC <- event: //往postC裏面寫入事件,上面我們說了readC和postC是同一個通道,意味着,我們往postC裏面寫入數據,readC就能收到對應的事件
   case <-s.closing: //關閉通道
   }
}
3、收到事件

訂閱方收到事件(以miner裏面的update方法爲例),可以看到這邊持有對應的readC通道,當往postC發送信息的時候,由於通道得特性,readC就會收到信息。

func (miner *Miner) update() {
   
   
   events := miner.mux.Subscribe(downloader.StartEvent{
   
   }, downloader.DoneEvent{
   
   }, downloader.FailedEvent{
   
   }) //訂閱事件,這裏
   defer func() {
   
   
      if !events.Closed() {
   
   
         events.Unsubscribe() //如果返回的時候沒關閉,取消訂閱並關閉通道
      }
   }()

   shouldStart := false
   canStart := true
   dlEventCh := events.Chan() //取到對應的訂閱通道 (`readC`)
   for {
   
   
      select {
   
   
      case ev := <-dlEventCh: //傳來了數據,第二步postC傳過來的
         if ev == nil {
   
    //取出事件爲nil,說明通道已被關閉
            // Unsubscription done, stop listening
            dlEventCh = nil
            continue
         }
         switch ev.Data.(type) {
   
   
         case downloader.StartEvent:
            wasMining := miner.Mining()
            miner.worker.stop()
            canStart = false
            if wasMining {
   
   
               // Resume mining after sync was finished
               shouldStart = true
               log.Info("Mining aborted due to sync")
            }
         case downloader.FailedEvent:
            canStart = true
            if shouldStart {
   
   
               miner.SetEtherbase(miner.coinbase)
               miner.worker.start()
            }
         case downloader.DoneEvent:
            canStart = true
            if shouldStart {
   
   
               miner.SetEtherbase(miner.coinbase)
               miner.worker.start()
            }
            // Stop reacting to downloader events
            events.Unsubscribe() //取消訂閱事件
         }
      case addr := <-miner.startCh:
         miner.SetEtherbase(addr)
         if canStart {
   
   
            miner.worker.start()
         }
         shouldStart = true
      case <-miner.stopCh:
         shouldStart = false
         miner.worker.stop()
      case <-miner.exitCh:
         miner.worker.close()
         return
      }
   }
}
4、取消訂閱

我們在上面一步看到了Unsubscribe方法,用它來進行事件得取消訂閱處理。

Unsubscribe方法

func (s *TypeMuxSubscription) Unsubscribe() {
   
   
   s.mux.del(s) //刪除對應slice中的訂閱對象
   s.closewait() //關閉通道等,做了一些收尾工作
}

del方法

func (mux *TypeMux) del(s *TypeMuxSubscription) {
   
   
   mux.mutex.Lock()
   defer mux.mutex.Unlock()
   for typ, subs := range mux.subm {
   
    //重複的找,直到所有的訂閱事件都被刪除,上面miner update方法就一個訂閱句柄對應了三個事件。
      if pos := find(subs, s); pos >= 0 {
   
   
         if len(subs) == 1 {
   
   
            delete(mux.subm, typ)
         } else {
   
   
            mux.subm[typ] = posdelete(subs, pos)
         }
      }
   }
}
5、總結

從上面得分析我們可以得出,由於newsub中構造得channel是無緩衝得,那麼發送事件就是阻塞得,這點就是官方引入Feed類型的原因。

2、Feed分析

a、主要結構體

Feed結構體

type Feed struct {
   
   
   once      sync.Once        // ensures that init only runs once 確保只初始化一次
   sendLock  chan struct{
   
   }    // sendLock has a one-element buffer and is empty when held.It protects sendCases. 緩衝,保護
   removeSub chan interface{
   
   } // interrupts Send 退出
   sendCases caseList         // the active set of select cases used by Send selectCase對象數組,用來發送

   // The inbox holds newly subscribed channels until they are added to sendCases.
   mu    sync.Mutex
   inbox caseList //新訂閱得,但是還沒被添加到sendCases得
   etype reflect.Type  //reflect類型,防止添加了不一樣的類型
}

caseList slice

type caseList []reflect.SelectCase

Subscription接口

type Subscription interface {
   
   
   Err() <-chan error // returns the error channel
   Unsubscribe()      // cancels sending of events, closing the error channel
}

feedSub結構體

type feedSub struct {
   
   
   feed    *Feed  //Feed結構體
   channel reflect.Value //通道
   errOnce sync.Once //關閉,只執行一次
   err     chan error
}

b、流程梳理

1、訂閱事件

在外部調用FeedSubscribe方法,進行訂閱,注意傳入得channel,它是核心。

Subscribe方法:

func (f *Feed) Subscribe(channel interface{
   
   }) Subscription {
   
   
   f.once.Do(f.init) //進行初始化,一個feed變量只進行一次

   chanval := reflect.ValueOf(channel)  //獲取到channel得reflect value
   chantyp := chanval.Type() //獲取到channel得reflect type
   if chantyp.Kind() != reflect.Chan || chantyp.ChanDir()&reflect.SendDir == 0 {
   
    //對類型進行判斷,不符合要求的進行panic,這裏不能爲 單向接收通道類型
      panic(errBadChannel)
   }
    //構造一個結構體
   sub := &feedSub{
   
   feed: f, channel: chanval, err: make(chan error, 1)}

   f.mu.Lock()
   defer f.mu.Unlock()
   if !f.typecheck(chantyp.Elem()) {
   
    //判斷類型是否相等
      panic(feedTypeError{
   
   op: "Subscribe", got: chantyp, want: reflect.ChanOf(reflect.SendDir, f.etype)})
   }
   // Add the select case to the inbox.
   // The next Send will add it to f.sendCases.
    //先將cas方法去到inbox,在下一次發送得時候,會加入到發送列表
   cas := reflect.SelectCase{
   
   Dir: reflect.SelectSend, Chan: chanval}
   f.inbox = append(f.inbox, cas)
   return sub
}
2、發送事件

發送方調用Send方法進行事件發佈(發佈是往訂閱得時候傳入得channel裏面寫入數據)

Send方法

// Send delivers to all subscribed channels simultaneously.
// It returns the number of subscribers that the value was sent to.
func (f *Feed) Send(value interface{
   
   }) (nsent int) {
   
   
   rvalue := reflect.ValueOf(value)  //取出reflect值

   f.once.Do(f.init) //在上面訂閱得時候初始化過,個人覺得這裏加上是爲了保險,防止未初始化
   <-f.sendLock  //保證串行發送

   // Add new cases from the inbox after taking the send lock.
   f.mu.Lock()
   f.sendCases = append(f.sendCases, f.inbox...) //將inbox裏面的加進來
   f.inbox = nil //indox清空

   if !f.typecheck(rvalue.Type()) {
   
    //類型檢查
      f.sendLock <- struct{
   
   }{
   
   } //寫回通道值,下次可以用
      f.mu.Unlock()
      panic(feedTypeError{
   
   op: "Send", got: rvalue.Type(), want: f.etype})
   }
   f.mu.Unlock()

   // Set the sent value on all channels.
    //這裏從第一個開始取(因爲sendCases第一個值,是一個單向接收通道類型,不能往裏面發送,所以過濾掉)
   for i := firstSubSendCase; i < len(f.sendCases); i++ {
   
   
      f.sendCases[i].Send = rvalue //將要發送得值寫入到selectCase裏面
   }

   // Send until all channels except removeSub have been chosen. 'cases' tracks a prefix
   // of sendCases. When a send succeeds, the corresponding case moves to the end of
   // 'cases' and it shrinks by one element.
   cases := f.sendCases //取出cases
   for {
   
   
      // Fast path: try sending without blocking before adding to the select set.
      // This should usually succeed if subscribers are fast enough and have free
      // buffer space.
       //先嚐試發送,如果沒有阻塞,應該就會成功;
      for i := firstSubSendCase; i < len(cases); i++ {
   
   
         if cases[i].Chan.TrySend(rvalue) {
   
    //嘗試發送
            nsent++
            cases = cases.deactivate(i) //將最後一個和當前i得值交換,並丟棄最後一個元素
            i--
         }
      }
       //如果長度爲一,說明只剩下單向接收通道了。就可以直接退出了。
      if len(cases) == firstSubSendCase {
   
   
         break
      }
      // Select on all the receivers, waiting for them to unblock.
      chosen, recv, _ := reflect.Select(cases)
      if chosen == 0 /* <-f.removeSub */ {
   
    //如果選中了單向接收通道,就將傳過來的值從sendCases中刪除
         index := f.sendCases.find(recv.Interface())
         f.sendCases = f.sendCases.delete(index) //找到對應的index,將對應的元素從sendCases中刪除
         if index >= 0 && index < len(cases) {
   
   
            // Shrink 'cases' too because the removed case was still active.
            cases = f.sendCases[:len(cases)-1] //收縮cases
         }
      } else {
   
   
         cases = cases.deactivate(chosen)//將最後一個和當前chosen得值交換,並丟棄最後一個元素
         nsent++
      }
   }

   // Forget about the sent value and hand off the send lock.
   for i := firstSubSendCase; i < len(f.sendCases); i++ {
   
   
      f.sendCases[i].Send = reflect.Value{
   
   } //發送完成,將所有發送成功的selectCase值置爲默認值
   }
   f.sendLock <- struct{
   
   }{
   
   } //往通道寫值,等待下一次發送
   return nsent //返回發送成功的個數
}
3、收到事件

訂閱方收到事件,我們訂閱得時候傳入得channel,這個時候就會收到數據。比如eth/protocols/snap/sync.go裏面的Sync方法

Sync方法

func (s *Syncer) Sync(root common.Hash, cancel chan struct{
   
   }) error {
   
   
   // Move the trie root from any previous value, revert stateless markers for
   // any peers and initialize the syncer if it was not yet run
   s.lock.Lock()
   .....
   // Keep scheduling sync tasks
   peerJoin := make(chan string, 16)
   peerJoinSub := s.peerJoin.Subscribe(peerJoin) //訂閱,傳入通道
   defer peerJoinSub.Unsubscribe() //取消訂閱

   peerDrop := make(chan string, 16)
   peerDropSub := s.peerDrop.Subscribe(peerDrop) //訂閱,傳入通道
   defer peerDropSub.Unsubscribe() //取消訂閱

   for {
   
   
      // Remove all completed tasks and terminate sync if everything's done
      .....
      // Wait for something to happen
      select {
   
   
      case <-s.update:
         // Something happened (new peer, delivery, timeout), recheck tasks
      case <-peerJoin: //收到數據,進行處理
         // A new peer joined, try to schedule it new tasks
      case id := <-peerDrop://收到數據,進行處理
         s.revertRequests(id)
      case <-cancel:
         return nil
		....
      }
      // Report stats if something meaningful happened
      s.report(false)
   }
}
4、取消訂閱

調用方調用Unsubscribe函數進行取消訂閱。如上面一步中所示。

Unsubscribe函數

func (sub *feedSub) Unsubscribe() {
   
   
   sub.errOnce.Do(func() {
   
    //只進行一次
      sub.feed.remove(sub)
      close(sub.err)
   })
}

從上面可以看出,主要調用了Feedremove方法

remove方法

func (f *Feed) remove(sub *feedSub) {
   
   
   // Delete from inbox first, which covers channels
   // that have not been added to f.sendCases yet.
   ch := sub.channel.Interface() //取出對應的通道值
   f.mu.Lock()
   index := f.inbox.find(ch) //看ch在indox裏面沒,在裏面直接刪除,就可以返回了。
   if index != -1 {
   
   
      f.inbox = f.inbox.delete(index)
      f.mu.Unlock()
      return
   }
   f.mu.Unlock()

   select {
   
   
   case f.removeSub <- ch: //正在發送.removeSub通道寫入,
      // Send will remove the channel from f.sendCases.
   case <-f.sendLock: //沒有正在發送得
      // No Send is in progress, delete the channel now that we have the send lock.
      f.sendCases = f.sendCases.delete(f.sendCases.find(ch)) //直接找到刪除
      f.sendLock <- struct{
   
   }{
   
   } //還給別人,方便下一個用
   }
}
5、總結

Feed利用反射來實現了發佈訂閱模式,他跟TypeMux得最大區別就是Feed是非阻塞式得。它會先嚐試進行發送,不行的話在進行阻塞得發送。共同點都是通過channel來進行數據得傳遞。

3、其他方式

a、封裝SubscriptionScope

結構體

type SubscriptionScope struct {
   
   
   mu     sync.Mutex
   subs   map[*scopeSub]struct{
   
   }
   closed bool
}

type scopeSub struct {
   
   
   sc *SubscriptionScope
   s  Subscription
}

通過Feed構造Subscription,在調用SubscriptionScopeTrack方法,進行了二次構造

Track方法

func (sc *SubscriptionScope) Track(s Subscription) Subscription {
   
   
   sc.mu.Lock()
   defer sc.mu.Unlock()
   if sc.closed {
   
   
      return nil
   }
   if sc.subs == nil {
   
   
      sc.subs = make(map[*scopeSub]struct{
   
   })
   }
   ss := &scopeSub{
   
   sc, s} //構造scopeSub結構體
   sc.subs[ss] = struct{
   
   }{
   
   }
   return ss
}

取消訂閱Unsubscribe

func (s *scopeSub) Unsubscribe() {
   
   
   s.s.Unsubscribe() //基礎的取消訂閱
   s.sc.mu.Lock()
   defer s.sc.mu.Unlock()
   delete(s.sc.subs, s) //從中刪除
}

b、封裝匿名函數

結構體

type funcSub struct {
   
   
   unsub        chan struct{
   
   }
   err          chan error
   mu           sync.Mutex
   unsubscribed bool
}

構造Subscription,主要邏輯由傳入得producer實現

func NewSubscription(producer func(<-chan struct{
   
   }) error) Subscription {
   
   
   s := &funcSub{
   
   unsub: make(chan struct{
   
   }), err: make(chan error, 1)}
   go func() {
   
   
      defer close(s.err)
      err := producer(s.unsub)
      s.mu.Lock()
      defer s.mu.Unlock()
      if !s.unsubscribed {
   
   
         if err != nil {
   
   
            s.err <- err
         }
         s.unsubscribed = true
      }
   }()
   return s
}

取消訂閱Unsubscribe

func (s *funcSub) Unsubscribe() {
   
   
   s.mu.Lock()
   if s.unsubscribed {
   
   
      s.mu.Unlock()
      return
   }
   s.unsubscribed = true
   close(s.unsub)
   s.mu.Unlock()
   // Wait for producer shutdown.
   <-s.err //這裏等待producer函數退出
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章