以太坊事件机制以及优化

1 以太坊的事件机制

以太坊go-ethereum源码中发送事件除了用常规的通道以外,还用了封装的Feed结构来执行事件的订阅和发送。以太坊中使用了大量的Feed来处理事件。使用Feed订阅事件的步骤是:

  • 定义一个通道ch:ch=make(someType)
  • 定义一个Feed对象feed
  • Feed订阅通道ch:feed.Subscribe(ch)
  • 使用feed发送数据给通道:feed.Send(someTypeData)
  • ch接收数据:ret<-ch

一个feed可以订阅多个通道,当使用feed发送数据后,所有的通道都将接收到数据。下文将解读Feed的源码,在进入Feed源码解读之前我们先介绍一下go中的reflect包中的SelectCase。

2 使用reflect.SelectCase来监听多个通道

对于多个通道ch1,ch2,ch3,使用传统的Select方式来监听:

package main
 
import (
	"fmt"
	"strconv"
)
 
func main() {
	var chs1 = make(chan int)
	var chs2 = make(chan float64)
	var chs3 = make(chan string)
	var ch4close = make(chan int)
	defer close(ch4close)
 
	go func(c chan int, ch4close chan int) {
		for i := 0; i < 5; i++ {
			c <- i
		}
		close(c)
		ch4close <- 1
	}(chs1, ch4close)
 
	go func(c chan float64, ch4close chan int) {
		for i := 0; i < 5; i++ {
			c <- float64(i) + 0.1
		}
		close(c)
		ch4close <- 1
	}(chs2, ch4close)
 
	go func(c chan string, ch4close chan int) {
		for i := 0; i < 5; i++ {
			c <- "string:" + strconv.Itoa(i)
		}
		close(c)
		ch4close <- 1
	}(chs3, ch4close)
 
	done := 0
	finished := 0
	for finished < 3 {
		select {
		case v, ok := <-chs1:
			if ok {
				done = done + 1
				fmt.Println(0, v)
			}
		case v, ok := <-chs2:
			if ok {
				done = done + 1
				fmt.Println(1, v)
			}
		case v, ok := <-chs3:
			if ok {
				done = done + 1
				fmt.Println(2, v)
			}
		case _, ok := <- ch4close:
			if ok {
				finished = finished+1
			}
		}
	}
	fmt.Println("Done", done)
}

使用reflect的方式来监听:

package main
 
import (
	"fmt"
	"reflect"
	"strconv"
)
 
func main() {
	var chs1 = make(chan int)
	var chs2 = make(chan float64)
	var chs3 = make(chan string)
	var ch4close = make(chan int)
	defer close(ch4close)
 
	go func(c chan int, ch4close chan int) {
		for i := 0; i < 5; i++ {
			c <- i
		}
		close(c)
		ch4close <- 1
	}(chs1, ch4close)
 
	go func(c chan float64, ch4close chan int) {
		for i := 0; i < 5; i++ {
			c <- float64(i) + 0.1
		}
		close(c)
		ch4close <- 1
	}(chs2, ch4close)
 
	go func(c chan string, ch4close chan int) {
		for i := 0; i < 5; i++ {
			c <- "string:" + strconv.Itoa(i)
		}
		close(c)
		ch4close <- 1
	}(chs3, ch4close)
 
 
	var selectCase = make([]reflect.SelectCase, 4)
	selectCase[0].Dir = reflect.SelectRecv
	selectCase[0].Chan = reflect.ValueOf(chs1)
 
	selectCase[1].Dir = reflect.SelectRecv
	selectCase[1].Chan = reflect.ValueOf(chs2)
 
	selectCase[2].Dir = reflect.SelectRecv
	selectCase[2].Chan = reflect.ValueOf(chs3)
 
	selectCase[3].Dir = reflect.SelectRecv
	selectCase[3].Chan = reflect.ValueOf(ch4close)
 
	done := 0
	finished := 0
	for finished < len(selectCase)-1 {
		chosen, recv, recvOk := reflect.Select(selectCase)
 
		if recvOk {
			done = done+1
			switch chosen {
			case 0:
				fmt.Println(chosen, recv.Int())
			case 1:
				fmt.Println(chosen, recv.Float())
			case 2:
				fmt.Println(chosen, recv.String())
			case 3:
				finished = finished+1
				done = done-1
				// fmt.Println("finished\t", finished)
			}
		}
	}
	fmt.Println("Done", done)
 
}

这里构建了一个reflect.SelectCase数组selectCase,将要监听的通道添加到数组中。监听时只要使用reflect.Select(selectCase)就可以监听所有通道的消息。当通道数多的时候,用SelectCase的方式将会更简洁优雅。

3 Feed源码解读

Feed结构的源码在event/feed.go中。

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

	// The inbox holds newly subscribed channels until they are added to sendCases.
	mu     sync.Mutex
	inbox  caseList
	etype  reflect.Type
	closed bool
}

type caseList []reflect.SelectCase

Feed结构核心的是inbox成员,它是一个SelectCase的数组,保存了该Feed订阅的所有通道。sendCase是所有活跃的通道数组。sendLock通道用来作为锁来保护sendCase。

初始化函数

func (f *Feed) init() {
	f.removeSub = make(chan interface{})
	f.sendLock = make(chan struct{}, 1)
	f.sendLock <- struct{}{}
	f.sendCases = caseList{{Chan: reflect.ValueOf(f.removeSub), Dir: reflect.SelectRecv}}
}

这里sendLock被设置成有容量为1的缓冲通道。并且给sendLock先写入了一个值。sendCases预先加入了removeSub通道作为第一个通道。

通道订阅函数


//这个通道需要有足够的缓冲空间以避免阻塞其它订阅者。速度慢的订阅者不会被丢弃
func (f *Feed) Subscribe(channel interface{}) Subscription {
	f.once.Do(f.init)

	chanval := reflect.ValueOf(channel)
	chantyp := chanval.Type()
	if chantyp.Kind() != reflect.Chan || chantyp.ChanDir()&reflect.SendDir == 0 {
		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 := reflect.SelectCase{Dir: reflect.SelectSend, Chan: chanval}
	f.inbox = append(f.inbox, cas)
	return sub
}

这个函数做的事情很简单,就是根据通道ch构造一个SelectCase对象,然后将其加入到inbox数组中。这样就完成了通道的订阅。

发送函数

// 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)

	f.once.Do(f.init)//重新初始化,onece.Do保证只会执行一次
	<-f.sendLock    //读sendLock通道,若sendLock为空则会堵塞

	// Add new cases from the inbox after taking the send lock.
	f.mu.Lock()    //访问公共变量加锁
	f.sendCases = append(f.sendCases, f.inbox...)//将inbox注入到sendCase
	f.inbox = nil 

	if !f.typecheck(rvalue.Type()) {
		f.sendLock <- struct{}{}    //出错了,退出前先写sendLock以免下次send操作堵塞
		panic(feedTypeError{op: "Send", got: rvalue.Type(), want: f.etype})
	}
	f.mu.Unlock()

	// 给所有通道设置要发送的数据
	for i := firstSubSendCase; i < len(f.sendCases); i++ {
		f.sendCases[i].Send = rvalue
	}

	// 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
	
	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++ {
           //首先使用TrySend进行发送,这是一种非阻塞操作。当订阅者足够快时一般能够立即成功
			if cases[i].Chan.TrySend(rvalue) {
				nsent++
				cases = cases.deactivate(i)//发送成功,后移该通道
				i--
			}
		}
		if len(cases) == firstSubSendCase {//所有通道发送完成,退出
			break
		}
		// Select on all the receivers, waiting for them to unblock.
		chosen, recv, _ := reflect.Select(cases)//等待通道返回
		//<-f.removeSub
		if chosen == 0  {
			index := f.sendCases.find(recv.Interface())
			f.sendCases = f.sendCases.delete(index)
			if index >= 0 && index < len(cases) {
				// Shrink 'cases' too because the removed case was still active.
				cases = f.sendCases[:len(cases)-1]
			}
		} else {
			cases = cases.deactivate(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{}
	}
	f.sendLock <- struct{}{}//返回时写入sendLock,为下次发送做准备
	return nsent
}

send函数使用通道的trySend方法来发送,在正常情况下能够立即发送成功,但是当接收通道堵塞的时候,则需要用Select方法这种堵塞的方式等待通道发送成功。在最后返回时,写入sendLock,为下次发送做准备。

4 send函数存在的问题及优化

我们看到send函数使用了sendLock通道,它是一个容量为1的通道。在send函数最开始,读出sendLock通道,如果这个时候sendLock为空,则send函数就会堵塞。所以在send函数最后,写入了sendLock通道,这样下次发送去读sendLock时就不会堵塞。看起来好像没有问题,但是理想很丰满,显示有时候会骨感。这里存在的问题就是chosen, recv, _ := reflect.Select(cases)这行代码可能会堵塞,导致for循环一值退不出,send函数发生堵塞,导致sendLock不会被写入。从而导致了死锁。下次send发送就会被堵塞。

扫描二维码关注公众号,回复: 4003728 查看本文章

这里使用sendLock是为了保护公共的sendCase数据,解决思路是去掉sendCase,不适用全局的sendCase,而使用局部变量。这样就不用考虑同步的问题了。改造后的send函数:

func (f *Feed) Send(value interface{}) (nsent int) {
	rvalue := reflect.ValueOf(value)

	f.once.Do(f.init)
	//<-f.sendLock

	sendCases := caseList{{Chan: reflect.ValueOf(f.removeSub), Dir: reflect.SelectRecv}}
	sendCases = append(sendCases, f.inbox...)

	// Set the sent value on all channels.
	for i := firstSubSendCase; i < len(sendCases); i++ {
		sendCases[i].Send = rvalue
	}

	// 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 := sendCases
	//LOOP:
	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--
			}
		}
		if len(cases) == firstSubSendCase {
			break
		}
		// Select on all the receivers, waiting for them to unblock.
		chosen, recv, _ := reflect.Select(cases)
		//<-f.removeSub
		if chosen == 0  {
			index := f.sendCases.find(recv.Interface())
			f.sendCases = f.sendCases.delete(index)
			if index >= 0 && index < len(cases) {
				// Shrink 'cases' too because the removed case was still active.
				cases = f.sendCases[:len(cases)-1]
			}
		} else {
			cases = cases.deactivate(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{}
	}
	//f.sendLock <- struct{}{}
	return nsent
}

某次send可能会堵塞,但是不会影响下次send发送。

5 go-ethereum源码中使用send的坑

 我们看core/blockchain.go中的发送函数PostChainEvents():

// PostChainEvents iterates over the events generated by a chain insertion and
// posts them into the event feed.
// TODO: Should not expose PostChainEvents. The chain events should be posted in WriteBlock.
func (bc *BlockChain) PostChainEvents(events []interface{}, logs []*types.Log) {
	log.Info("lzj-log PostChainEvents", "events len",len(events))
	// post event logs for further processing
	if logs != nil {
		bc.logsFeed.Send(logs)
	}
	for _, event := range events {
		switch ev := event.(type) {
		case ChainEvent:
			log.Info("lzj-log send ChainEvent")
			bc.chainFeed.Send(ev)

		case ChainHeadEvent:
			log.Info("lzj-log send ChainHeadEvent")
			bc.chainHeadFeed.Send(ev)

		case ChainSideEvent:
			log.Info("lzj-log send ChainSideEvent")
			bc.chainSideFeed.Send(ev)
		}
	}
}

这个函数是在for循环中先后发送了ChainEvent、ChainHeadEvent和ChainSideEvent事件。在insert函数中调用了这个 函数。但是这里有个问题,如果前一个事件发送堵塞了,后面的事件发送就不会执行。需要把Send函数放到单独的协程中去。改成这样可以防止堵塞的问题:

// PostChainEvents iterates over the events generated by a chain insertion and
// posts them into the event feed.
// TODO: Should not expose PostChainEvents. The chain events should be posted in WriteBlock.
func (bc *BlockChain) PostChainEvents(events []interface{}, logs []*types.Log) {
	log.Info("lzj-log PostChainEvents", "events len",len(events))
	// post event logs for further processing
	if logs != nil {
		bc.logsFeed.Send(logs)
	}
	for _, event := range events {
		switch ev := event.(type) {
		case ChainEvent:
			log.Info("lzj-log send ChainEvent")
			go bc.chainFeed.Send(ev)

		case ChainHeadEvent:
			log.Info("lzj-log send ChainHeadEvent")
			go bc.chainHeadFeed.Send(ev)

		case ChainSideEvent:
			log.Info("lzj-log send ChainSideEvent")
			go bc.chainSideFeed.Send(ev)
		}
	}
}

在go里面使用通道要发非常小心,因为很容易引起堵塞从而达不到自己期望的结果。

猜你喜欢

转载自blog.csdn.net/liuzhijun301/article/details/83893198