面试题答案
一键面试方案设计与代码实现
可以使用sync.RWMutex
来解决这个问题。sync.RWMutex
允许有多个读操作并发执行,但写操作必须是独占的。
package main
import (
"fmt"
"sync"
)
var (
data []int
mu sync.RWMutex
wg sync.WaitGroup
)
func reader(id int) {
defer wg.Done()
mu.RLock()
fmt.Printf("Reader %d reading: %v\n", id, data)
mu.RUnlock()
}
func writer(id int, value int) {
defer wg.Done()
mu.Lock()
data = append(data, value)
fmt.Printf("Writer %d wrote: %d\n", id, value)
mu.Unlock()
}
func main() {
for i := 0; i < 3; i++ {
wg.Add(1)
go writer(i, i*10)
}
for i := 0; i < 5; i++ {
wg.Add(1)
go reader(i)
}
wg.Wait()
}
方案原理
- 读锁(RLock):多个读操作可以同时获取读锁,这意味着多个
goroutine
可以同时读取切片,因为读操作本身不会修改数据,所以不会产生数据竞争。 - 写锁(Lock):写操作必须获取写锁,写锁是独占的。当一个
goroutine
获取了写锁,其他goroutine
无论是读还是写操作,都必须等待写锁释放。这样就保证了写操作的原子性,避免了数据竞争。
性能和可扩展性考量
- 性能:
- 读性能:由于多个读操作可以并发执行,在高读低写的场景下,性能表现良好。读锁的获取和释放开销相对较小。
- 写性能:写操作是独占的,在高写场景下,可能会导致其他
goroutine
长时间等待,从而影响整体性能。
- 可扩展性:
- 读扩展性:随着读
goroutine
数量的增加,系统的读处理能力可以相应提升,因为读操作可以并发执行。 - 写扩展性:写操作的独占性限制了写操作的并发度,在高并发写场景下,可扩展性较差。
- 读扩展性:随着读
不使用锁机制的其他方法
- 通道(Channel):
- 原理:通过通道在
goroutine
之间传递数据,而不是直接读写共享切片。一个goroutine
负责接收来自其他goroutine
的数据,并写入切片,其他goroutine
只负责向通道发送数据。这样可以避免数据竞争,因为所有的写操作都由一个goroutine
统一处理。 - 示例代码:
- 原理:通过通道在
package main
import (
"fmt"
"sync"
)
var (
data []int
ch = make(chan int)
wg sync.WaitGroup
)
func writer() {
defer wg.Done()
for value := range ch {
data = append(data, value)
fmt.Printf("Writer wrote: %d\n", value)
}
}
func main() {
wg.Add(1)
go writer()
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
ch <- id * 10
}(i)
}
close(ch)
wg.Wait()
fmt.Println("Final data:", data)
}
- 优点:避免了锁带来的开销,在某些场景下性能更好,并且更符合Go语言的“不要通过共享内存来通信,而要通过通信来共享内存”的理念。
- 缺点:增加了代码的复杂性,特别是在需要处理复杂的读写逻辑时。同时,通道的缓冲管理也需要额外注意。
- 不可变数据结构:
- 原理:每次写操作都创建一个新的切片,而不是修改原来的切片。读操作始终读取旧的切片,直到新的切片创建完成并替换旧的切片。这种方式避免了数据竞争,因为读操作不会受到写操作的影响。
- 示例代码:
package main
import (
"fmt"
"sync"
)
var (
currentData []int
wg sync.WaitGroup
)
func writer(id int, value int) {
defer wg.Done()
newData := append([]int{}, currentData...)
newData = append(newData, value)
currentData = newData
fmt.Printf("Writer %d wrote: %d\n", id, value)
}
func reader(id int) {
defer wg.Done()
fmt.Printf("Reader %d reading: %v\n", id, currentData)
}
func main() {
for i := 0; i < 3; i++ {
wg.Add(1)
go writer(i, i*10)
}
for i := 0; i < 5; i++ {
wg.Add(1)
go reader(i)
}
wg.Wait()
}
- 优点:不需要锁机制,简单直观。
- 缺点:每次写操作都创建新的切片,可能会导致内存开销较大,特别是在数据量较大或者写操作频繁的场景下。