不定期更新

sync.WaitGroup 小坑

错误写法一

  • 在线运行: https://play.golang.org/p/j_daqJR_9Ca
  • 错误: 13 行,在 goroutine 中执行 wg.Add(1),也许 goroutine 还没来及执行 Add(1) 已经执行了 Wait 操作
  • 表现: 输出结果不固定,可能只输出 exit,可能输出几个值。
package main

import (
	"log"
	"sync"
)

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")
}

输出

$ go run a.go
2019/07/01 16:48:52 exit
$ go run a.go
2019/07/01 16:48:55 i:1
2019/07/01 16:48:55 exit
$ go run a.go
2019/07/01 16:48:57 i:3
2019/07/01 16:48:57 i:1
2019/07/01 16:48:57 i:2
2019/07/01 16:48:57 exit

错误写法二

  • 在线运行: https://play.golang.org/p/j_daqJR_9Ca
  • 错误: 在 16 行,其实是把 wg 的拷贝传递到了 goroutine 中(值拷贝),导致 Done 操作是在 wg 的副本执行的。因此 Wait 就死锁了。

在线运行或者本地格式化代码时用了 go vet,那么就会提示你 func passes lock by value: sync.WaitGroup contains sync.noCopyWaitGroup 结构中有一个 noCopy结构(在第一次使用后不可复制,使用go vet作为检测使用),保证 A WaitGroup must not be copied after first use。主要是为了在使用时出现拷贝 wg 副本的问题。

但是拷贝 &wg 是没有问题的,因为具体使用时还是会指到其具体地址。

package main

import (
	"log"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(wg sync.WaitGroup, i int) {
			log.Printf("i:%d", i)
			wg.Done()
		}(wg, i)
	}

	wg.Wait()

	log.Println("exit")
}

输出

$ go run a.go
2019/07/01 17:09:22 i:0
2019/07/01 17:09:22 i:3
2019/07/01 17:09:22 i:1
2019/07/01 17:09:22 i:4
2019/07/01 17:09:22 i:2
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000088008)
	/usr/local/go/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0xc000088000)
	/usr/local/go/src/sync/waitgroup.go:130 +0x65
exit status 2
$ go run a.go
2019/07/01 17:09:24 i:0
2019/07/01 17:09:24 i:4
2019/07/01 17:09:24 i:2
2019/07/01 17:09:24 i:3
panic: sync: negative WaitGroup counter

goroutine 20 [running]:
sync.(*WaitGroup).Add(0xc00008e034, 0xffffffffffffffff)
	/usr/local/go/src/sync/waitgroup.go:74 +0x135
sync.(*WaitGroup).Done(...)
	/usr/local/go/src/sync/waitgroup.go:99
main.main.func1(0x400000000, 0xc000000000, 0x3)

正确示例

写法一

package main

import (
	"log"
	"sync"
)

func main() {
	wg := &sync.WaitGroup{}

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(wg *sync.WaitGroup, i int) {
			log.Printf("i:%d", i)
			wg.Done()
		}(wg, i)
	}

	wg.Wait()

	log.Println("exit")
}

写法二

package main

import (
	"log"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(i int) {
			log.Printf("i:%d", i)
			wg.Done()
		}(i)
	}

	wg.Wait()

	log.Println("exit")
}

sync.WaitGroup 小坑

错误示例

  • 在线运行: https://play.golang.org/p/mRjdkTCrUnE
  • 错误: 前一个 Wait 还没有完成就 Add 也会 panic(具体原因会以后通过源码进行解释)
  • Note: that calls with a positive delta that occur when the counter is zero must happen before a Wait. Calls with a negative delta, or calls with a positive delta that start when the counter is greater than zero, may happen at any time.
package main

import (
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		time.Sleep(time.Millisecond)
		wg.Done()
		wg.Add(1)
	}()
	wg.Wait()
}

输出

$ go run a.go
panic: sync: WaitGroup is reused before previous Wait has returned

goroutine 1 [running]:
sync.(*WaitGroup).Wait(0xc00006e000)
	/usr/local/go/src/sync/waitgroup.go:132 +0xae
exit status 2

正确示例

sync.Waitgroup 内部维护了一个计数器来记录 waiter

  • 在 Wait 之前可以设置这个计数器
  • 在这个计数器为 0 的时候,所有等待的 goroutine 都会解出等待,继续执行
  • Add 方法可以增加计数,也可以传入负值减少计数,但是计数器小于 0 的情况会 panic
  • Done 是利用 Add(-1) 实现的,所以如果 Done 的次数多余计数器会 panic
  • Wait 多次调用没有问题,只要计数器为 0,他就不会阻塞
  • 并发 Add 和 Wait 会 panic
  • 前一个 Wait 还没有完成就 Add 也会 panic
  • Waitgroup 可以重用,但是要等前一个 Wait 完成后重用
  • 推荐使用方法是,先统一Add,在goroutine里并发的Done,然后Wait

channel 接收者没有及时初始化

错误示例

第 25 行的 time.Sleep(5 * time.Second) 是为了模拟接收者没有及时接收。

  • 在线运行: https://play.golang.org/p/ae7VN5u1Dpz
  • 错误: 当接收者没有及时接收就会导致所有的 goroutine 都是退出状态
  • 注意: source 一定要有 default 方法,如果 channel 中缓存位置不够,则会导致协程一直是阻塞状态,造成内存泄露
package main

import (
	"fmt"
	"math/rand"
	"time"
)

func source(c chan<- int32) {
	rb := rand.Intn(3)
	time.Sleep(time.Duration(rb) * time.Second)
	select {
	case c <- 1:
	default:
	}
}

func main() {
	rand.Seed(time.Now().UnixNano())

	c := make(chan int32)
	for i := 0; i < 5; i++ {
		go source(c)
	}
	time.Sleep(5 * time.Second)
	rnd := <-c // 只采用第一个成功发送的回应数据
	fmt.Println(rnd)
}

输出

$ go run a.go
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
exit status 2

正确示例

只要 13 行初始化的 channel 换成有缓存的即可

c := make(chan int32, 1)

return 和 defer

今天看了 Go语言学习——彻底弄懂return和defer的微妙关系 所以直接整理一下

package main

import "fmt"

func main() {
	fmt.Println("f1 result: ", f1())
	fmt.Println("f2 result: ", f2())
}

func f1() int {
	var i int
	defer func() {
		i++
		fmt.Println("f11: ", i)
	}()

	defer func() {
		i++
		fmt.Println("f12: ", i)
	}()

	i = 1000
	return i
}

func f2() (i int) {
	defer func() {
		i++
		fmt.Println("f21: ", i)
	}()

	defer func() {
		i++
		fmt.Println("f22: ", i)
	}()

	i = 1000
	return i
}

运行结果

$ go run a.go
f12:  1001
f11:  1002
f1 result:  1000
f22:  1001
f21:  1002
f2 result:  1002
  • defer 的执行顺序是先进后出
  • f1 流程
    • 声明 i 变量,默认初始为 int 的零值 0
    • 执行 i=1000 的赋值操作
    • 执行return语句,返回 i 的值
    • 真正返回之前执行 defer,两个 defer 分别执行自增(defer 顺序先进后出)
  • f2 流程
    • 已经定义了返回值 i,直接赋值 i=1000
    • 在返回前执行 defer

问题: 无名的返回值返回是 1000,没有受到 defer 自增的影响。但是有名的返回值执行 defer 后却受到了影响,返回了 1002?

结论: 原因就是return会将返回值先保存起来,对于无名返回值来说,保存在一个临时对象中,defer是看不到这个临时对象的;而对于有名返回值来说,就保存在已命名的变量中。

参考链接