MST

星途 面试题库

面试题:Go信号量与其他并发控制机制的深度比较及应用场景分析

Go语言中有多种并发控制机制,如互斥锁、读写锁、通道、信号量等。请详细对比信号量与其他并发控制机制在原理、性能、适用场景等方面的差异,并举例说明在何种复杂业务场景下信号量是最优选择,且要分析为何其他机制不适用。
31.9万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

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 访问,会导致爬虫效率极低,不符合需求。
    • 读写锁:此场景并非读写问题,读写锁不适用。
    • 通道:通道主要用于数据传递和同步,难以直接控制并发请求数量,虽然可以通过一些复杂的方法模拟信号量,但不如信号量直接和简洁。