Questions - concurrency
1. 有缓冲和无缓冲的 Channel 区别
- 无缓冲 Channel:
- 发送:发送时若无接收者则阻塞,直到接收者接收
- 接收:接收时若无发送者则阻塞,直到发送者发送
- 有缓冲 Channel:
- 发送:仅在缓冲以满时阻塞
- 接收:仅在缓冲为空时阻塞
func main() {
st := time.Now()
ch := make(chan bool)
go func () {
time.Sleep(time.Second * 2)
<-ch
}()
ch <- true // 无缓冲,发送方阻塞直到接收方接收到数据。
fmt.Printf("cost %.1f s\n", time.Now().Sub(st).Seconds())
time.Sleep(time.Second * 5)
}
func main() {
st := time.Now()
ch := make(chan bool, 2)
go func () {
time.Sleep(time.Second * 2)
<-ch
}()
ch <- true
ch <- true // 缓冲区为 2,发送方不阻塞,继续往下执行
fmt.Printf("cost %.1f s\n", time.Now().Sub(st).Seconds()) // cost 0.0 s
ch <- true // 缓冲区使用完,发送方阻塞,2s 后接收方接收到数据,释放一个插槽,继续往下执行
fmt.Printf("cost %.1f s\n", time.Now().Sub(st).Seconds()) // cost 2.0 s
time.Sleep(time.Second * 5)
}
2. 简述 协程泄露(Goroutine Leak)
Goroutine Leak 只得时 Goroutine 在创建之后无法停止并被回收,最终导致内存泄露与程序崩溃。
常见场景:
- 因通道阻塞导致 goroutine 陷入等待,无法结束
func query() int {
ch := make(chan int)
for i := 0; i < 1000; i++ {
go func() { ch <- 0 }()
}
return <-ch
}
func main() {
for i := 0; i < 4; i++ {
query()
fmt.Printf("goroutines: %d\n", runtime.NumGoroutine())
}
}
- 出现死锁 因 goroutine 之间出现死锁,导致 goroutine 阻塞
- 无限循环 因 goroutine 陷入无限循环中,没有通知 goroutine 停止导致持续运行
3. GOMAXPROCS 作用
The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit.
GOMAXPROCS
限制的是同时执行用户态 Go 代码的操作系统线程的数量,但是对于被系统调用阻塞的线程数量是没有限制的。
GOMAXPROCS
的默认值等于 CPU 的逻辑核数,同一时间,一个核只能绑定一个线程,然后运行被调度的协程。
使用环境变量GOMAXPROCS
或函数runtime.GOMAXPROCS
设置。
如 GOMAXPROCS 超过 CPU 核心数,因为同时运行的线程最大为 CPU 核心数,此时反而增加线程切换的开销,降低性能。
对于 I/O 密集型应用,可以适当增大该值以提高 I/O 吞吐率。
4. mutex 有几种模式
mutex 有 正常模式 和 饥饿模式:
- 正常模式:Goroutine 按照 FIFO 顺序获取锁;刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁。
- 饥饿模式:新 Goroutine 无法获取锁,无法进入自旋,而是在队列末尾等待。
- 一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式,以保证互斥锁的公平性。
- 若一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式。
5. Goroutine 何时发生内存泄露
在Go中内存泄露分为暂时性内存泄露和永久性内存泄露。
暂时性内存泄露
- 获取长字符串中的一段导致长字符串未释放
- 获取长slice中的一段导致长slice未释放
- 在长slice新建slice导致泄漏
永久性内存泄露
- goroutine永久阻塞而导致泄漏
- time.Ticker未关闭导致泄漏
- 不正确使用Finalizer(Go版本的析构函数)导致泄漏
6. Go 的竞争条件
所谓竞态竞争,就是当两个或以上的goroutine访问相同资源时候,对资源进行读/写。
可以用go run -race xx.go
来进行检测。
解决方法是,对临界区资源上锁,或者使用原子操作(atomics),原子操作的开销小于上锁。
7. 如果若干个goroutine,有一个panic会怎么做?
有一个panic,那么剩余goroutine也会退出,程序退出。如果不想程序退出,那么必须通过调用 recover() 方法来捕获 panic 并恢复将要崩掉的程序。
8. defer 可以捕获子 goroutine 的 panic 么
不可以,必须在子协程中捕获后进行处理或抛出。