并发编程题
Go 面试中的并发控制、顺序打印、生产消费与超时实现。
如何用不超过 10 个 goroutine 处理 100 个元素?
回答重点
“最多 10 个 goroutine”和“最多 10 个任务同时执行”不是同一要求:
- 固定 worker pool 只创建 10 个工作 goroutine。
- 每个任务创建一个 goroutine 再用信号量限流,会创建 100 个 goroutine,只限制并发执行数。
题目明确限制 goroutine 数量时,应使用固定 worker pool:
package main
import (
"fmt"
"sync"
)
func main() {
const workerCount = 10
jobs := make(chan int)
var wg sync.WaitGroup
for workerID := 0; workerID < workerCount; workerID++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for value := range jobs {
fmt.Printf("worker=%d value=%d\n", id, value)
}
}(workerID)
}
for value := 0; value < 100; value++ {
jobs <- value
}
close(jobs)
wg.Wait()
}每个元素只会被一个 worker 接收,因此不会重复处理;输出顺序不保证与输入顺序一致。
如何让三个 goroutine 依次打印 abc,重复 10 次?
回答重点
为每个阶段使用一个 token channel,只有拿到 token 的 goroutine 才能输出,并把 token 传给下一阶段。
package main
import (
"fmt"
"sync"
)
func main() {
a := make(chan struct{})
b := make(chan struct{})
c := make(chan struct{})
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
<-a
fmt.Print("a")
b <- struct{}{}
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
<-b
fmt.Print("b")
c <- struct{}{}
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
<-c
fmt.Print("c")
if i < 9 {
a <- struct{}{}
}
}
}()
a <- struct{}{}
wg.Wait()
fmt.Println()
}最后一轮不能继续向 a 发送,否则没有接收者,会导致死锁。
如何用单个 Channel 让两个 goroutine 交替打印 0 和 1?
回答重点
可以让两个 goroutine 在同一个无缓冲 channel 上完成握手。打印 0 的 goroutine 先输出再发送,打印 1 的 goroutine先接收再输出;每轮末尾再反向确认。
package main
import (
"fmt"
"sync"
)
func main() {
turn := make(chan struct{})
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
fmt.Print(0)
turn <- struct{}{}
<-turn
}
}()
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
<-turn
fmt.Print(1)
turn <- struct{}{}
}
}()
wg.Wait()
fmt.Println()
}这个方案依赖两端操作顺序不对称。如果两个 goroutine 都先接收,它们会一起阻塞;如果由主 goroutine 先投递 token,则第一次由谁接收是不确定的。
如何用 sync.Cond 实现有界生产者消费者队列?
回答重点
sync.Cond 必须与锁和条件循环配合使用。Wait 会原子地解锁并挂起,返回前重新加锁;醒来后必须重新检查条件,因为条件可能已被其他 goroutine 改变。下面用同一个 Cond 等待“非空”和“未满”两个条件,因此状态变化后使用 Broadcast;生产环境也可以拆成两个条件变量,减少无效唤醒。
package main
import (
"fmt"
"sync"
)
type queue struct {
cond *sync.Cond
values []int
limit int
closed bool
}
func newQueue(limit int) *queue {
return &queue{
cond: sync.NewCond(&sync.Mutex{}),
limit: limit,
}
}
func (q *queue) put(value int) bool {
q.cond.L.Lock()
defer q.cond.L.Unlock()
for len(q.values) == q.limit && !q.closed {
q.cond.Wait()
}
if q.closed {
return false
}
q.values = append(q.values, value)
q.cond.Broadcast()
return true
}
func (q *queue) get() (int, bool) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
for len(q.values) == 0 && !q.closed {
q.cond.Wait()
}
if len(q.values) == 0 {
return 0, false
}
value := q.values[0]
q.values = q.values[1:]
q.cond.Broadcast()
return value, true
}
func (q *queue) close() {
q.cond.L.Lock()
q.closed = true
q.cond.Broadcast()
q.cond.L.Unlock()
}
func main() {
q := newQueue(2)
go func() {
for i := 0; i < 5; i++ {
q.put(i)
}
q.close()
}()
for value, ok := q.get(); ok; value, ok = q.get() {
fmt.Println(value)
}
}如果只需要普通有界队列,带缓冲 channel 已经实现了生产者消费者同步,通常比 sync.Cond 更简单。
如何限制并发度并让整批任务最多执行 1 秒?
回答重点
使用固定数量的 worker 控制并发度,用 context.WithTimeout 通知生产者和 worker 停止。取消是协作式的,任务函数也必须检查 Context 才能及时退出。
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
jobs := make(chan int)
var wg sync.WaitGroup
for workerID := 0; workerID < 10; workerID++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
fmt.Printf("worker=%d job=%d\n", id, job)
}
}
}(workerID)
}
sendLoop:
for job := 0; job < 1000; job++ {
select {
case <-ctx.Done():
break sendLoop
case jobs <- job:
}
}
close(jobs)
wg.Wait()
}原题常见错误是创建 1000 个 goroutine,却声称“控制 1000 个并发”。应先明确限制的是任务总数、goroutine 数量还是同时执行数。
如何让两个 goroutine 输出 a1b2c3?
回答重点
仍然用两个 token channel 表达轮次:
package main
import (
"fmt"
"sync"
)
func main() {
letterTurn := make(chan struct{})
numberTurn := make(chan struct{})
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for letter := 'a'; letter <= 'c'; letter++ {
<-letterTurn
fmt.Printf("%c", letter)
numberTurn <- struct{}{}
}
}()
go func() {
defer wg.Done()
for number := 1; number <= 3; number++ {
<-numberTurn
fmt.Print(number)
if number < 3 {
letterTurn <- struct{}{}
}
}
}()
letterTurn <- struct{}{}
wg.Wait()
fmt.Println()
}面试官追问
- 如果某个 worker 提前返回,如何避免其余 goroutine 永久阻塞?
- 如何加入 Context 取消并保证所有 channel 只关闭一次?
- 输出顺序和任务完成顺序都要保持时,如何设计结果重排?
评论