面试题答案
一键面试1. 同步原语差异比较
实现原理
- 互斥锁(Mutex):通过一个简单的二元状态(锁定或未锁定)来控制对共享资源的访问。当一个 goroutine 获取到锁,其他 goroutine 必须等待锁释放才能获取。
- 读写锁(RWMutex):区分读操作和写操作。允许多个 goroutine 同时进行读操作,因为读操作不会修改数据,不会产生数据竞争。但写操作时,必须独占锁,防止其他读写操作。
- 信号量(Semaphore):通过一个计数器来控制并发访问的数量。当计数器大于0时,goroutine 可以获取信号量(计数器减1),当计数器为0时,获取操作会阻塞直到有信号量释放(计数器加1)。
- 条件变量(Cond):通常与互斥锁配合使用。它允许 goroutine 等待特定条件满足后再继续执行。通过通知机制,当条件满足时,唤醒等待的 goroutine。
适用场景
- 互斥锁:适用于对共享资源的读写操作都需要独占访问的场景,防止数据竞争。
- 读写锁:适用于读多写少的场景,读操作并发执行,写操作独占执行,提高并发性能。
- 信号量:用于控制同时访问某个资源或执行某个任务的 goroutine 数量。
- 条件变量:当 goroutine 需要等待某个条件满足才能继续执行时使用,例如生产者 - 消费者模型中,消费者等待生产者生产数据。
性能
- 互斥锁:简单直接,但在高并发场景下,如果竞争激烈,会导致较多的等待时间,性能下降。
- 读写锁:读多写少场景下性能较好,读操作并发执行。但写操作时会阻塞所有读写操作,写操作频繁时性能不佳。
- 信号量:性能取决于其控制的并发数量,合理设置可有效控制资源使用,但如果设置不当,可能导致性能瓶颈。
- 条件变量:本身性能开销较小,但依赖于等待条件的复杂度和通知频率。如果条件频繁不满足,会有较多等待开销。
2. 高并发读写场景设计
设计思路
在高并发读写场景中,使用读写锁来控制读写操作,读操作并发执行,写操作独占执行。同时,引入条件变量来处理写操作时的特殊情况,例如当写操作需要等待某些条件(如数据达到一定量)时。互斥锁用于保护共享资源的状态,防止并发修改。
示例代码
package main
import (
"fmt"
"sync"
"time"
)
type Data struct {
sync.RWMutex
sync.Cond
value int
threshold int
}
func NewData(threshold int) *Data {
mu := &sync.Mutex{}
return &Data{
RWMutex: sync.RWMutex{},
Cond: sync.Cond{L: mu},
value: 0,
threshold: threshold,
}
}
func (d *Data) Read() int {
d.RLock()
defer d.RUnlock()
return d.value
}
func (d *Data) Write(newValue int) {
d.Lock()
for d.value < d.threshold {
d.Wait()
}
d.value = newValue
d.Unlock()
fmt.Println("Write completed")
}
func (d *Data) Increment() {
d.Lock()
d.value++
fmt.Printf("Incremented to %d\n", d.value)
if d.value >= d.threshold {
d.Broadcast()
}
d.Unlock()
}
func main() {
data := NewData(5)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
data.Increment()
}()
}
go func() {
wg.Wait()
data.Write(100)
}()
time.Sleep(2 * time.Second)
fmt.Println("Final read:", data.Read())
}
在上述代码中,Data
结构体包含读写锁 RWMutex
、条件变量 Cond
、共享数据 value
和阈值 threshold
。Read
方法使用读锁读取数据,Write
方法使用条件变量等待 value
达到 threshold
后写入新值,Increment
方法增加 value
并在达到阈值时广播通知等待的 Write
操作。通过这种方式综合使用同步原语,在高并发读写场景中实现较好的性能和资源利用率。