golang bug 记录:select关键字 与 time.After()的陷阱

615 阅读3分钟

bug review

前两天在做mit6.824 lab2a的时候一直卡在了一个奇怪的bug上,代码如下图

mit6.284 lab2a select time.After bug.png

  • 在进行test的用例中会将一个leader与其他server进行分区,那此时就会进行一轮新的elect,而担任新term中的candidate依然会向旧leader发出vote请求,以避免旧leader网络恢复时联系不上新leader,因为在向旧leader进行vote的请求是不可达的,也就是说该请求会在网络timeout才进行返回,而这这个timeout通常比其他的flower节点的election period长很多,那么如果该vote不能够返回那么就会导致countdowmlatch.wait不能被触发,也就会导致整个选举周期延长,然后就会导致已经votefor 自己的flower节点election period到期也进行选举。所以在这个情况下我需要进行超时处理,这里采用了time.After()进行超时处理。
  • 大致的情况就是在使用select监听sendRequestVote方法时其进行一个超时处理。也就是在这样的情况下奇怪的事情发生了。无论我的time.After的duration设置为多小都有几率不被触发。

有的同学可能不了解我这个lab,所以我写了一个比较小的demo来重现这个bug,代码如下:

func main() {
    after := time.After(time.Millisecond * 100)
    ch := make(chan bool1)
    for {
    select {
        case <-after:
        fmt.Println("after")
        return
        case ch <- timeoutFunction():
        fmt.Println("return")
        return
        }
    }
 }
 func timeoutFunction() bool {
    time.Sleep(time.Millisecond * 200)
    return true
}

多次执行程序会得到: after return return after 这样交替输出的情况。

bug分析

经过一番折腾,如更换go版本,尝试添加default case,尝试在select外套上for循环等操作皆无果,最后在一篇知乎专栏找到的答案。有兴趣的可以去看一下。 产生bug的原因大概是这样:因为select操作会在主线程中一次调用每个case中的方法,然后再进行判断是否有case被唤醒了,然后进入该case。问题就出在调用方法这一步,因为我的case 2采用的是chan<-timeoutFunction()阻塞,那么按照这个逻辑,主线程将会在timeoutFunction方法中进行阻塞,然后再进行返回,而此时返回后两个case都已经被唤醒了,那么select将会随机选择一个case进行调用。

解决方案

将同步阻塞的返回值修改为chan bool,然后在select中的case进行与time.After相同的 读chan操作。 修改的代码如下:

func main() {
    after := time.After(time.Millisecond * 100)
    ch := make(chan bool1)
    for {
    select {
        case <-after:
        fmt.Println("after")
        return
        case <- timeoutFunction():
        fmt.Println("return")
        return
        }
    }
 }
 func timeoutFunction() chan bool 
    ch:=make(chan bool,1)
    time.Sleep(time.Millisecond * 200)
    ch<-true
    return ch
}

此时还有一个问题,就是在timeoutFunction方法中依然会同步阻塞然后才会进行返回,依然存在问题,所以需要进一步将time.Sleep()进行异步操作,然后直接返回chan,此时才能真正利用chan的读操作就进行阻塞。

func main() {
    after := time.After(time.Millisecond * 100)
    ch := make(chan bool1)
    for {
    select {
        case <-after:
        fmt.Println("after")
        return
        case <- timeoutFunction():
        fmt.Println("return")
        return
        }
    }
 }
 func timeoutFunction() chan bool {
    ch:=make(chan bool,1)
    go func(){
        time.Sleep(time.Millisecond * 200)
        ch<-true
    }()
    return ch 
}

至此这个bug就完全解决了。

总结

这次debug属实酸爽,差不多翻了一整天日志文件。不过最后好在还是解决了,究其原因就是将select 与 time.After进行组合使用时需要注意不要再case中执行长时间的任务,而导致time.After失效。这里我也总结了一个比较实用的技巧,就是在这种情况不要用wirte chan 等待函数的一个返回值,而要获取函数返回的一个chan然后对其进行read chan阻塞操作。