面试题答案
一键面试Goroutine和线程的资源分配机制不同点
- 资源占用:
- 线程:线程一般占用相对较多的资源,每个线程都有自己独立的栈空间,其栈大小在创建时通常就确定(如在Linux系统下,默认栈大小可能是几MB)。线程的创建、销毁开销也较大。
- Goroutine:Goroutine非常轻量级,占用资源极少。它的栈空间初始时非常小(一般只有2KB左右),并且可以根据需要动态增长和收缩。
- 调度方式:
- 线程:操作系统内核级调度,调度开销大,上下文切换涉及用户态到内核态的转换,会消耗较多CPU时间。线程调度是抢占式的,由操作系统内核管理。
- Goroutine:Go语言运行时(runtime)调度,属于用户态调度(M:N调度模型,即多个Goroutine映射到多个操作系统线程上)。Goroutine调度在用户空间内完成,上下文切换开销小,调度是协作式的(cooperative scheduling),通过Go语言的运行时系统实现,当Goroutine执行系统调用、I/O操作或主动调用
runtime.Gosched()
等函数时,才会让出CPU,让其他Goroutine有机会执行。
Go运行时系统管理线程资源确保高效执行的方式
- M:N调度模型:Go运行时使用M:N调度模型,即多个Goroutine(M个)映射到多个操作系统线程(N个)上。运行时系统维护一个Goroutine队列,每个操作系统线程(称为M)从队列中获取Goroutine并执行。这种方式可以充分利用多核CPU,并且在一个Goroutine阻塞(如进行I/O操作)时,不会阻塞对应的操作系统线程,该线程可以去执行队列中的其他Goroutine。
- P(Processor)的引入:Go运行时引入了P(Processor)的概念,P代表了能执行Goroutine的资源,它包含了一个本地Goroutine队列。M必须关联到一个P才能执行Goroutine。运行时系统会根据CPU核心数来分配P的数量,默认情况下,P的数量等于CPU核心数。这样每个CPU核心都可以高效地执行一组Goroutine,避免了多个M竞争同一个P导致的资源浪费。例如,在一个4核CPU的机器上,默认会创建4个P,每个P负责调度一部分Goroutine,从而提高了整体的并发执行效率。
可能出现的资源竞争问题及解决方法
- 资源竞争问题示例:
package main
import (
"fmt"
)
var counter int
func increment() {
counter++
}
func main() {
for i := 0; i < 1000; i++ {
go increment()
}
fmt.Println("Final counter value:", counter)
}
在上述代码中,多个Goroutine同时调用increment
函数对counter
变量进行自增操作。由于counter++
操作不是原子的,在并发执行时可能会出现资源竞争问题,导致最终counter
的值小于1000。
2. 解决方法:
- 互斥锁(Mutex):使用sync.Mutex
来保护共享资源。修改上述代码如下:
package main
import (
"fmt"
"sync"
)
var counter int
var mu sync.Mutex
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Final counter value:", counter)
}
在increment
函数中,通过mu.Lock()
和mu.Unlock()
来保证在同一时刻只有一个Goroutine能够访问和修改counter
变量,从而避免资源竞争。
- 读写锁(RWMutex):如果共享资源读操作远多于写操作,可以使用sync.RWMutex
。读操作时可以多个Goroutine同时进行,写操作时则需要独占锁。例如:
package main
import (
"fmt"
"sync"
)
var data int
var rwmu sync.RWMutex
func read() int {
rwmu.RLock()
defer rwmu.RUnlock()
return data
}
func write(newData int) {
rwmu.Lock()
data = newData
rwmu.Unlock()
}
- **原子操作**:对于一些简单的数值类型(如`int`、`int64`等),可以使用`sync/atomic`包提供的原子操作函数。例如,修改上述`increment`函数如下:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var counter int64
func increment() {
atomic.AddInt64(&counter, 1)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Final counter value:", counter)
}
atomic.AddInt64
函数保证了对counter
的自增操作是原子的,避免了资源竞争。