面试题答案
一键面试1. 原理对比
- 互斥锁(Mutex): 原理是通过锁定和解锁操作,保证同一时间只有一个 goroutine 能访问共享资源。当一个 goroutine 获得锁后,其他 goroutine 必须等待锁被释放才能获取。例如:
package main
import (
"fmt"
"sync"
)
var (
mu sync.Mutex
count int
)
func increment(wg *sync.WaitGroup) {
mu.Lock()
count++
mu.Unlock()
wg.Done()
}
- 读写锁(RWMutex): 允许有多个读操作同时进行,但写操作时会独占资源,阻止其他读和写操作。读锁可以被多个 goroutine 同时获取,写锁则是排他的。例如:
package main
import (
"fmt"
"sync"
)
var (
rwmu sync.RWMutex
data int
)
func read(wg *sync.WaitGroup) {
rwmu.RLock()
fmt.Println("Read data:", data)
rwmu.RUnlock()
wg.Done()
}
func write(wg *sync.WaitGroup) {
rwmu.Lock()
data++
rwmu.Unlock()
wg.Done()
}
- 通道(Channel): 是一种用于 goroutine 之间通信和同步的机制。通过在通道上发送和接收数据来实现同步,数据传递会阻塞直到接收方准备好接收。例如:
package main
import (
"fmt"
)
func sender(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
func receiver(ch chan int) {
for val := range ch {
fmt.Println("Received:", val)
}
}
- 信号量(Semaphore): 信号量通过一个计数器来控制对资源的访问。计数器表示可用资源的数量,当一个 goroutine 获取信号量时,计数器减一;释放信号量时,计数器加一。当计数器为 0 时,获取操作会阻塞。例如:
package main
import (
"fmt"
"sync"
"time"
)
type Semaphore struct {
count int
ch chan struct{}
}
func NewSemaphore(count int) *Semaphore {
s := &Semaphore{
count: count,
ch: make(chan struct{}, count),
}
for i := 0; i < count; i++ {
s.ch <- struct{}{}
}
return s
}
func (s *Semaphore) Acquire() {
<-s.ch
}
func (s *Semaphore) Release() {
s.ch <- struct{}{}
}
func worker(s *Semaphore, id int, wg *sync.WaitGroup) {
s.Acquire()
fmt.Printf("Worker %d acquired semaphore\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d released semaphore\n", id)
s.Release()
wg.Done()
}
2. 性能对比
- 互斥锁:适用于读写操作都可能修改共享资源的场景,但由于同一时间只有一个 goroutine 能访问,在高并发读操作场景下性能较差。
- 读写锁:在多读少写场景下性能较好,因为读操作可以并发进行。但写操作时会阻塞所有读和其他写操作,在写操作频繁时性能不佳。
- 通道:性能取决于通信的频率和数据量。如果数据传递频繁且数据量较大,可能会有性能开销。
- 信号量:当需要控制并发访问的数量时,信号量性能较好。相比互斥锁和读写锁,它可以更灵活地控制同时访问资源的 goroutine 数量。
3. 适用场景对比
- 互斥锁:适用于保护共享资源,防止竞态条件,在读写操作都可能修改资源的情况下使用。
- 读写锁:适用于多读少写的场景,如缓存数据的读取。
- 通道:适用于 goroutine 之间需要进行数据传递和同步的场景,如生产者 - 消费者模型。
- 信号量:适用于限制同时访问资源的 goroutine 数量的场景,例如限制数据库连接数、限制并发请求数等。
4. 信号量最优选择的复杂业务场景及分析
场景:一个爬虫程序,需要爬取大量网页。由于目标服务器对同一 IP 的并发请求数有限制(假设为 10 个),过多请求会导致服务器拒绝连接。
- 信号量适用原因:信号量可以轻松控制同时发起请求的 goroutine 数量为 10 个,保证不会超过服务器限制。通过在每个爬虫 goroutine 开始前获取信号量,结束后释放信号量来实现。
package main
import (
"fmt"
"sync"
"time"
)
type Semaphore struct {
count int
ch chan struct{}
}
func NewSemaphore(count int) *Semaphore {
s := &Semaphore{
count: count,
ch: make(chan struct{}, count),
}
for i := 0; i < count; i++ {
s.ch <- struct{}{}
}
return s
}
func (s *Semaphore) Acquire() {
<-s.ch
}
func (s *Semaphore) Release() {
s.ch <- struct{}{}
}
func crawler(s *Semaphore, url string, wg *sync.WaitGroup) {
s.Acquire()
fmt.Printf("Crawling %s\n", url)
time.Sleep(time.Second)
fmt.Printf("Finished crawling %s\n", url)
s.Release()
wg.Done()
}
func main() {
urls := []string{"url1", "url2", "url3", "url4", "url5", "url6", "url7", "url8", "url9", "url10", "url11"}
var wg sync.WaitGroup
sem := NewSemaphore(10)
for _, url := range urls {
wg.Add(1)
go crawler(sem, url, &wg)
}
wg.Wait()
}
- 其他机制不适用分析:
- 互斥锁:互斥锁一次只允许一个 goroutine 访问,会导致爬虫效率极低,不符合需求。
- 读写锁:此场景并非读写问题,读写锁不适用。
- 通道:通道主要用于数据传递和同步,难以直接控制并发请求数量,虽然可以通过一些复杂的方法模拟信号量,但不如信号量直接和简洁。