面试题答案
一键面试Mutex(互斥锁)
- 优点:
- 简单直接:实现简单,易于理解和使用。在只需要保证同一时刻只有一个 goroutine 访问共享资源时,Mutex 是最直观的选择。
- 通用场景:适用于各种读写操作混合且没有明显读写特性区分的场景,对资源访问的控制最为基础和全面。
- 缺点:
- 性能瓶颈:如果读操作频繁,每次读都要获取 Mutex 锁,会导致读操作串行化,降低系统并发性能。因为同一时刻只允许一个 goroutine 进入临界区,即使是读操作也会相互阻塞。
- 适用场景:
- 读写混合且无明显特性:例如一个简单的计数器,既有读操作获取当前计数值,也有写操作增加或减少计数值,使用 Mutex 就足够简单有效。
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment(wg *sync.WaitGroup) {
mu.Lock()
counter++
mu.Unlock()
wg.Done()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Final counter:", counter)
}
RWMutex(读写锁)
- 优点:
- 读并发:允许多个 goroutine 同时进行读操作,只有写操作时才需要独占锁。当读操作远远多于写操作时,能显著提高系统并发性能。
- 缺点:
- 复杂:相比 Mutex,使用起来更复杂,需要根据读写操作的特性来正确使用读锁和写锁。
- 写操作阻塞:写操作时会阻塞所有读操作和其他写操作,可能导致写操作延迟。如果写操作频繁,整体性能会受影响。
- 适用场景:
- 多读少写:如缓存系统,大量的 goroutine 会读取缓存数据,只有偶尔的更新操作。
package main
import (
"fmt"
"sync"
)
var (
data = make(map[string]string)
rwMutex sync.RWMutex
)
func read(key string) string {
rwMutex.RLock()
value := data[key]
rwMutex.RUnlock()
return value
}
func write(key, value string) {
rwMutex.Lock()
data[key] = value
rwMutex.Unlock()
}
func main() {
go write("k1", "v1")
go func() {
fmt.Println("Read value:", read("k1"))
}()
}
sync.Semaphore(信号量,Go 1.19+)
- 优点:
- 资源限制:可以有效限制同时访问共享资源的 goroutine 数量,适用于需要控制并发数的场景,比如数据库连接池,限制同时连接数据库的 goroutine 数量。
- 缺点:
- 场景特定:功能相对特定,不像 Mutex 那样通用,在不需要控制并发数的场景下使用较为繁琐。
- 适用场景:
- 资源有限场景:例如限制同时处理的任务数量,防止资源耗尽。
package main
import (
"context"
"fmt"
"sync"
"time"
"golang.org/x/sync/semaphore"
)
func main() {
sem := semaphore.NewWeighted(2)
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
if err := sem.Acquire(context.Background(), 1); err != nil {
fmt.Println("Acquire error:", err)
return
}
defer sem.Release(1)
fmt.Printf("Goroutine %d is working\n", id)
time.Sleep(2 * time.Second)
}(i)
}
wg.Wait()
}
总结:在简单的读写混合场景且对性能要求不是极致的情况下,Mutex 是不错的选择,因其简单易用。而在多读少写的场景下,RWMutex 能大幅提升并发读性能;当需要限制并发访问数量以保护有限资源时,sync.Semaphore 则是合适的选择。