Golang: Interview Question: Programming: Concurrency 2

 10th November 2022 at 3:29pm

给出下面几个例子,问面试者其中哪些例子有问题。事实上每一个都有 bug,看看面试者能看出几个。

例子 1

func finishReq(timeout time.Duration) ob {
	ch := make(chan ob)
	go func() {
		result := fn()
		ch <- result
	}()
	select {
	case result := <-ch:
		return result
	case <-time.After(timeout):
		return nil
	}
}

问题:如果超时返回 nil 了,ch 永远无法被写入,造成本函数中起的协程泄露。

解决方法:将 ch 改成 buffered channel;ch := make(chan ob, 1)

例子 2

var group sync.WaitGroup
group.Add(len(pm.plugins))
for _, p := range pm.plugins {
	go func(p *plugin) {
		// do something with p...
		defer group.Done()
	}
	group.Wait()
}

这个很简单,group.Wait() 不应该放在 for 循环中,而是应该挪到最后一行。

例子 3

hctx, hcancel := context.WithCancel(ctx)
if timeout > 0 {
	hctx, hcancel = context.WithTimeout(ctx, timeout)
}

这个比较隐晦。如果 timeout > 0,第一行产生的 hctx 没有被 cancel,会造成协程泄露。

改成:

var (
	hctx    context.Context
	hcancel context.CancelFunc
)
if timeout > 0 {
	hctx, hcancel = context.WithTimeout(ctx, timeout)
} else {
	hctx, hcancel = context.WithCancel(ctx)
}

例子 4

for i := 1; i <= 5; i++ {
	go func() {
		fmt.Println(i)
	}()
}

会输出什么?

答案是:一般是输出 5 个 5。有可能其中有一个 4,要看调度的情况。但很少会是预计的 1 2 3 4 5。

改成:

for i := 1; i <= 5; i++ {
	go func(i int) {
		fmt.Println(i)
	}(i)
}

例子 5

var wg sync.WaitGroup
for i := 0; i < 5; i++ {
	go func() {
		wg.Add(1)

		// do something...
		wg.Done()
	}()
}

wg.Wait()

这个例子在于 wg.Add(1) 不能放在新协程中去做。假如 for 循环中的新起的协程一个都还没被调度到,代码就运行到了 wg.Wait() 这一行,那么实际上 wg.Wait() 并没有起到等所有协程结束的作用。改成:

var wg sync.WaitGroup
for i := 0; i < 5; i++ {
	wg.Add(1)
	go func() {
		// do something...
		wg.Done()
	}()
}

wg.Wait()

例子 6

func (c *Client) Close() {
	select {
	case <- c.closed:
	default:
		close(c.closed)
	}
}

乍一看这个实现没啥问题:select 语句中有一个 case 是从一个关闭的 channel 中接收消息时,该 case 总是 ready 的。

但是假如有并发调用 Close() 时,如果这两个协程都走入了 default 分支,会造成 channel 被关闭两次,引起 panic。

正确的写法是:

func (c *Client) Close() {
	c.closeOnce.Do(func() {
		close(c.closed)
	})
}

例子 7

ticker := time.NewTicker(interval)
for {
	doHeavyWork()

	select {
	case <- stopCh:
		return
	case <- ticker.C:
	}
}

这段代码的用意是,不停地执行 doHeavyWork() 直至收到 stopCh 消息退出。

它的问题很隐晦。问题出在 select 的机制上:如果有多个 case 是 ready 状态,会随机选择一个 case 去执行。这就导致了当 stopChticker.C 都可读时,随机选到了 ticker.C 分支,导致 doHeavyWork() 又被不必要地执行了一次。

解决方法是,在 doHeavyWork() 前再读一次 stopCh

ticker := time.NewTicker(interval)
for {
	select {
	case <- stopCh:
		return
	default:
	}

	doHeavyWork()

	select {
	case <- stopCh:
		return
	case <- ticker.C:
	}
}