Defer及WaitGroup问题详解|青训营笔记

301 阅读2分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记

一、Defer问题

defer数据结构:

type _defer struct {
    sp      uintptr   //函数栈指针
    pc      uintptr   //程序计数器
    fn      *funcval  //函数地址
    link    *_defer   //指向自身结构的指针,用于链接多个defer
}

defer后面一定要接一个函数,所以defer的数据结构更一般函数类似,也有栈指针、程序计数器、函数地址等等。与函数不同的是它含有一个指针,可用于指向另一个defer,每个goroutine数据结构中实际上也有一个defer指针,该指针指向一个defer的链表,每次声明一个defer时就将defer插入到单链表表头,每次执行defer就从单链表表头取出一个defer执行。 defer链表1.png 如图所示,新声明的defer(B())总是添加到链表头部,函数返回前执行defer则是从链表首部依次取出执行,形成一个栈结构。

defer的功能:

defer用来声明一个延迟函数,可以定义多个延时函数,这些函数会放入到一个栈中,当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。

通常用defer来做一些资源的释放,比如关闭io操作。

defer使用有几个需注意的点:

详情可查阅本人博客:defer使用细节

二、WaitGroup问题

1、waitGroup对象做值传递

如:

func main(){
    var swg sync.WaitGroup
    for i:=0;i<3;i++{
        swg.Add(1)
        go func(wg sync.WaitGroup,mark int){
            defer wg.Done()
            fmt.Printf("%d goroutine finish \n",mark)
        }(swg,i)
    }
    swg.Wait()
}
//运行结果:
//    0 goroutine finish 
//    1 goroutine finish 
//    2 goroutine finish 
//    fatal error: all goroutines are asleep - deadlock!

子协程中传入的waitGroup对象的一份新值拷贝,在main主协程的waitGroup对象并没有被调用Done()方法,导致标志位无法被释放,最后发生死锁。

解决方法:将waitGroup对象做参数传递时,使用其引用拷贝传入。

2、仅在子协程goroutine中进行add操作

如:

func main() {
    wg := sync.WaitGroup{}
    for i := 0; i < 5; i++ {
        go func(wg *sync.WaitGroup, i int) {
            wg.Add(1)
            log.Printf("i:%d", i)
            wg.Done()
        }(&wg, i)
    }
    wg.Wait()
    log.Println("exit")
}
//运行结果(可能出现):
//    2022/05/09 09:38:36 exit

因为子协程跟main主协程同步进行,可能子协程中还没来得及add(1),mian主线程就已经执行结束了。

解决方法:尽量不要仅在子协程中进行add操作。