Go 并发编程测试分析

June 14, 2019

Go 并发编程测试分析

本文是对https://colobu.com/2019/04/28/go-concurrency-quizzes/晁岳攀老师博客中提到的并发测试例子的讲解。

我按问题类型来分类说明错误原因。

所以P中没有可以调度的G时就会出现死锁

这个问题涉及到的题目为:1、Mutex2、RWMutex.

1、Mutex

package main
import (
	"fmt"
	"sync"
)
var mu sync.Mutex
var chain string
func main() {
	chain = "main"
	A()
	fmt.Println(chain)
}
func A() {
	mu.Lock()
	defer mu.Unlock()
	chain = chain + " --> A"
	B()
}
func B() {
	chain = chain + " --> B"
	C()
}
func C() {
	mu.Lock()
	defer mu.Unlock()
	chain = chain + " --> C"
}
$ go run main.go
fatal error: all goroutines are asleep - deadlock!

这一题的问题是runtime中只有一个G,但是当运行到A()方法是以及加锁,后面的C()方法接着去加锁,就会出现拿不到锁,于是这个G的状态就变为不可运行,所以就出现了deadlock!.

2、RWMutex

package main
import (
	"fmt"
	"sync"
	"time"
)
var mu sync.RWMutex
var count int
func main() {
	go A()
	time.Sleep(2 * time.Second)
	mu.Lock()
	defer mu.Unlock()
	count++
	fmt.Println(count)
}
func A() {
	mu.RLock()
	defer mu.RUnlock()
	B()
}
func B() {
	time.Sleep(5 * time.Second)
	C()
}
func C() {
	mu.RLock()
	defer mu.RUnlock()
}
$ go run main.go
fatal error: all goroutines are asleep - deadlock!

这一题的原因也是一样的,GA休眠之后状态就会变为等待,此时,主G去那锁也没有那到,就会变为不可运行状态,并让出cpu,此时所有的G都不可运行就出现死锁了。

WaitGroup 使用问题

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 main.go
panic: sync: WaitGroup is reused before previous Wait has returned

原因是多调用了一个wg.Add(1)

4、双检查实现单例

package doublecheck
import (
	"sync"
)
type Once struct {
	m    sync.Mutex
	done uint32
}
func (o *Once) Do(f func()) {
	if o.done == 1 {
		return
	}
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		o.done = 1
		f()
	}
}

这一题争议最大,博主是想告诉我们类似于Java里面缓存变量的问题,我觉得这里是不会出现问题的,因为对象是指针调用;运行本身没有问题,但是会出现数据竞争。使用go run -race main.go运行就会发现有数据竞争。

5、同步对象使用后不能被拷贝

package main
import (
	"fmt"
	"sync"
)
type MyMutex struct {
	count int
	sync.Mutex
}
func main() {
	var mu MyMutex
	mu.Lock()
	var mu2 = mu
	mu.count++
	mu.Unlock()
	mu2.Lock()
	mu2.count++
	mu2.Unlock()
	fmt.Println(mu.count, mu2.count)
}
$ go run main.go
fatal error: all goroutines are asleep - deadlock!

这个原因就是同步对象使用过之后不能再被拷贝,如果上面把mu.Lock()var mu2 = mu这两行进行交换一下就可以了。 使用过后不可以复制的对象有:


// A Cond must not be copied after first use.
type Cond struct 
// A Map must not be copied after first use.
type Map struct
// A Mutex must not be copied after first use.
type Mutex struct
// A Pool must not be copied after first use.
type Pool struct
// A RWMutex must not be copied after first use.
type RWMutex struct
// A WaitGroup must not be copied after first use.
type WaitGroup struct

sync包下的struct除了Once这个结构体其他的使用过后都不能被复制。不能被复制也包括函数传递参数,比如如下的使用是错误的:

func main{
    var wa sync.WaitGroup
    for i:=0 ;i < 10 ;i++ {
        wa.Add(1)
        go func(wa sync.WaitGroup){
            fmt.Println("wa.Down()")
            wa.Down()
        }(wa)
    }
    wa.Wait()
}

上面的代码就有问题,首先wa对象以及使用了wa.Add(1),后面开启一个go时确做参数传入,此时传入的是一个副本,就会出现不能正确的执行wa.Down();此处可以修改为传递指针go func(wa *sync.WaitGroup)或者使用闭包的方式使用WaitGroup对象。

7、channel

package main
import (
	"fmt"
	"runtime"
	"time"
)
func main() {
	var ch chan int
	go func() {
		ch = make(chan int, 1)
		ch <- 1
	}()
	go func(ch chan int) {
		time.Sleep(time.Second)
		<-ch
	}(ch)
	c := time.Tick(1 * time.Second)
	for range c {
		fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine())
	}
}
$ go run main.go
#goroutines: 2
#goroutines: 2
#goroutines: 2

这一题比较简单,但是我还是忽略了一个关键点,最后运行中有两个g,原因就是time.Tick其实是开启了一个G来计时的,然后通过Channel来通知。

我们来看一下time.Tick里面的实现:

time 包
func NewTicker(d Duration) *Ticker{
    ...
    startTimer(&t.r)// 这个方法对应到runtime.startTimer()方法上
}
runtime 包 
// startTimer adds t to the timer heap.
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
	if raceenabled {
		racerelease(unsafe.Pointer(t))
	}
	addtimer(t)
}
//在addtimer()方法中启动了一个`G`。

13、for range 问题

package main
import (
	"fmt"
	"sync"
	"time"
)
type T struct {
	V int
}
func (t *T) Incr(wg *sync.WaitGroup) {
	t.V++
	wg.Done()
}
func (t *T) Print() {
	time.Sleep(1e9)
	fmt.Print(t.V)
}
func main() {
	var wg sync.WaitGroup
	wg.Add(10)
	var ts = make([]T, 10)
	for i := 0; i < 10; i++ {
		ts[i] = T{i}
	}
	for _, t := range ts {
		go t.Incr(&wg)
	}
	wg.Wait()
	for _, t := range ts {
		go t.Print()
	}
	time.Sleep(5 * time.Second)
}
$ go run main.go
999999999

这一题可以是因为使用了for range的方式,这种方式中的t只是一个变量,会一直在边,当使用go t.Incr()的时候,此时的t已经变为了最后一个值,所以输出都是9,这个在使用map时也有这个问题,得到 的k/v只是值的一个拷贝。


LRF 记录学习、生活的点滴