几段 Go 并发代码

几段 Go 并发代码

可以使用 go run --race main.go 来验证代码中是否存在并发问题

for range

for i,v := range slice {
  // ...
  // go func() ...
}

在 for range 中,i, v 这两个变量仅仅被初始化一次,在之后在每轮 for 循环中都会修改 i 和 v 的值,所以如果在 for 循环里面开启一个 goroutine 引用 i 和 v 的话,由于 for range 所在的协程在对 i, v 进行写操作, 每轮 for 循环的内部的协程对 i, v 进行读操作,很容易导致并发问题。

解决方法是,在 for 循环内部声明个临时变量,将 i, v 的值赋值给该临时变量,而循环内部的协程就不再直接引用 i, v 了,而是引用该临时变量。

第一个版本
type T struct {
   FieldA string
}

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}
	wg.Add(elementCount)

	for _, t := range sliceT {
		go func() {
			defer wg.Done()
			t.FieldA = "hello"
		}()
	}
	wg.Wait()
}
修复后
type T struct {
	FieldA string
}

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}
	wg.Add(elementCount)

	for _, t := range sliceT {
		localT := t
		go func() {
			defer wg.Done()
			localT.FieldA = "hello"
		}()
	}

	wg.Wait()
}
第二个版本
type T struct {
	FieldA string
}

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}
	wg.Add(elementCount)

	for i := range sliceT {
		go func() {
			defer wg.Done()
			sliceT[i].FieldA = "hello"
		}()
	}

	wg.Wait()
}
修复后
type T struct {
	FieldA string
}

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}
	wg.Add(elementCount)

	for i := range sliceT {
		localI := i
		go func() {
			defer wg.Done()
			sliceT[localI].FieldA = "hello"
		}()
	}

	wg.Wait()
}

sync.WaitGroup

WaitGroup 封装了一个计数器并提供了三个 api ,分别是 Add(), Done(), Wait()。

Add(n int) 将会给计数器增加 n。

Done() 将会给计数器减 1。

Wait() 将会一直阻塞,直到计数器的值变成 0。

下面是一个常见错误:

type T struct {
	FieldA string
}

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}

	for i := range sliceT {
		localI := i
		go func() {
			wg.Add(1)
			defer wg.Done()
			sliceT[localI].FieldA = "hello"
		}()
	}
	
	wg.Wait()
}

这段程序不能保证 go func() 里面的代码一定会被执行。

因为 wg.Add(1) 是在子协程里面做的,程序完全可以执行完 for 循环之后,在 wg.Wait() 直接返回,不给子协程执行的机会。而由于 wg 里面的计数器为 0,wg.Wait() 并不会被阻塞。

由此得到一个很重要的结论: wg.Add() 必须和 wg.Wait() 在同一个协程里面被调用

正确的表述应该是 wg.Add() 和 wg.Wait() 之间必须要存在一个 happens before 关系,因为下面这个代码也是 ok 的。

wg.Add(1)
go func(){
  wg.Wait()
}
// do something
wg.Done()
封装 WaitGroup

有时我们并不想显示地调用 wg.Add() 。我们仅仅想并发执行几段代码,并用 wg.Wait() 等待这几段代码全部执行完成。

看看下面的程序

type T struct {
	FieldA string
}

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}
	
	wg := sync.WaitGroup{}
	wgFunc := func(do func()) {
		wg.Add(1)
		defer wg.Done()
		do()
	}

  for i := range sliceT {
		localI := i
    go wgFunc(func() {
			sliceT[localI].FieldA = "hello"
		})
	}

	wg.Wait()
}

wgFunc 方法内部引用了 wg,每次调用时会执行 wg.Add(1) 和 defer wg.Done() 来为并发保价护航。在 for 循环内部每次会开启一个协程去调用 wgFunc()。

但是它犯了和前一段代码一样的错误。

正确的封装应该是:

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}
	wgFunc := func(do func()) {
		wg.Add(1)
		go func() {
			defer wg.Done()
			do()
		}()
	}

	for i := range sliceT {
		localI := i
		wgFunc(func() {
			sliceT[localI].FieldA = "hello"
		})
	}

	wg.Wait()
}

再重复一次,wg.Add() 必须和 wg.Wait() 在同一个协程内被执行。

PS: golang.org/x/sync/errgroup 提供了比 sync.WaitGroup 功能更加强大的 errgroup.Group,用它,用它!

多个协程并发读写同一个变量时一定需要使用同步机制么?

不一定。

stack overflow 上面的一个问题: Can I concurrently write different slice elements

The rule is simple: if multiple goroutines access a variable concurrently, and at least one of the accesses is a write, then synchronization is required.

Structured variables of array, slice, and struct types have elements and fields that may be addressed individually. Each such element acts like a variable.

数组里面的每个元素/结构体里面的每个字段是能被独立寻址的,它们都可以视为一个独立的变量!

因此上面的程序虽然有很多协程在访问同一个 slice,但是由于这些协程访问的是 slice 上不同的元素。且 slice 上的每个元素只被一个协程访问,因此是安全的。

同理,多个协程访问结构体的不同字段也是安全的。

type T struct {
	FieldA string
	FieldB string
}

func main() {
	sliceT := make([]*T, 1000)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}
	wgFunc := func(do func()) {
		wg.Add(1)
		go func() {
			defer wg.Done()
			do()
		}()
	}

	for i := range sliceT {
		t := sliceT[i]
		wgFunc(func() {
			t.FieldA = "hello"
		})
		wgFunc(func() {
			t.FieldB = "world"
		})
	}

	wg.Wait()
}
posted @ 2022-01-09 00:09  机智的小小帅  阅读(155)  评论(0编辑  收藏  举报