面试题答案
一键面试可能出现的问题
- 数据竞争:多个 goroutine 同时读写切片,可能导致数据竞争,使程序出现未定义行为,结果不可预测。例如,一个 goroutine 正在读取切片时,另一个 goroutine 对切片进行了扩容或插入操作,可能导致读取到不一致的数据。
- 脏读:在并发环境下,一个 goroutine 可能读取到另一个 goroutine 尚未完全更新完成的切片数据,导致读取到“脏”数据。
解决方案及优缺点分析
1. 使用互斥锁(sync.Mutex)
实现方式:在对切片进行读写操作前,先获取互斥锁,操作完成后释放互斥锁。
package main
import (
"fmt"
"sync"
)
var (
slice []int
mutex sync.Mutex
wg sync.WaitGroup
)
func addElement(num int) {
defer wg.Done()
mutex.Lock()
slice = append(slice, num)
mutex.Unlock()
}
func readElement() {
defer wg.Done()
mutex.Lock()
for _, v := range slice {
fmt.Println(v)
}
mutex.Unlock()
}
优点:
- 简单直观,容易理解和实现。
- 适用于各种类型的并发读写操作。 缺点:
- 性能瓶颈,当并发量高时,锁的竞争会导致性能下降,因为同一时间只有一个 goroutine 能访问切片。
- 可能导致死锁,如果在获取锁的逻辑中出现错误,例如重复获取锁或未释放锁。
2. 使用读写锁(sync.RWMutex)
实现方式:读操作使用读锁,写操作使用写锁。多个 goroutine 可以同时获取读锁进行读取,但写操作时需要独占写锁。
package main
import (
"fmt"
"sync"
)
var (
slice []int
rwMutex sync.RWMutex
wg sync.WaitGroup
)
func addElement(num int) {
defer wg.Done()
rwMutex.Lock()
slice = append(slice, num)
rwMutex.Unlock()
}
func readElement() {
defer wg.Done()
rwMutex.RLock()
for _, v := range slice {
fmt.Println(v)
}
rwMutex.RUnlock()
}
优点:
- 读性能较好,允许多个 goroutine 同时进行读操作,提高了并发读的效率。
- 相比于互斥锁,在读多写少的场景下,性能提升明显。 缺点:
- 实现比互斥锁复杂一些,需要区分读锁和写锁的使用场景。
- 写操作仍然是独占的,当写操作频繁时,会影响整体性能,因为写锁会阻塞所有读操作和其他写操作。
3. 使用 channel 进行数据传递
实现方式:通过 channel 在 goroutine 之间传递对切片的操作请求,由一个专门的 goroutine 负责实际的切片操作,避免多个 goroutine 直接操作切片。
package main
import (
"fmt"
"sync"
)
type Op struct {
add int
read bool
}
var (
slice []int
ch = make(chan Op)
wg sync.WaitGroup
)
func operator() {
for op := range ch {
if op.read {
for _, v := range slice {
fmt.Println(v)
}
} else {
slice = append(slice, op.add)
}
}
close(ch)
}
func addElement(num int) {
defer wg.Done()
ch <- Op{add: num}
}
func readElement() {
defer wg.Done()
ch <- Op{read: true}
}
优点:
- 遵循 Go 语言“不要通过共享内存来通信,而要通过通信来共享内存”的原则,代码更符合 Go 的设计理念。
- 减少了锁的使用,在高并发场景下性能更好,因为 channel 本身就是线程安全的。 缺点:
- 实现相对复杂,需要设计合理的 channel 通信机制和操作流程。
- 对于复杂的切片操作,可能需要设计更复杂的消息结构和处理逻辑。