Go并发系列:3扩展同步原语-3.2 Semaphore(信号量)

125 阅读3分钟

3.2 Semaphore(信号量)

信号量 (Semaphore) 是一种用于控制对公共资源访问的计数器,可以用于限制同时访问资源的数量。信号量在并发编程中非常有用,特别是在需要限制并发访问的场景中。Go 语言标准库中没有直接提供信号量,但可以通过 sync 包和 channel 来实现类似功能。下面我们详细介绍信号量的概念、使用方法及示例。

3.2.1 什么是Semaphore

Semaphore 是一种同步原语,用于限制同时访问某一资源的数量。信号量维护了一个计数器,用于记录当前可用的资源数量。信号量有两种操作:

  • Acquire (P 操作):请求资源,如果计数器大于0,则计数器减1,否则等待。
  • Release (V 操作):释放资源,计数器加1,如果有等待的线程,则唤醒其中一个。

信号量可以用于实现各种并发控制机制,如资源池、连接池等。

3.2.2 Go语言中的信号量实现

虽然 Go 标准库中没有直接提供信号量,但我们可以使用 sync 包和 channel 来实现。以下是一个简单的信号量实现:

package main

import (
    "fmt"
    "sync"
    "time"
)

type Semaphore struct {
    ch chan struct{}
}

func NewSemaphore(limit int) *Semaphore {
    return &Semaphore{
        ch: make(chan struct{}, limit),
    }
}

func (s *Semaphore) Acquire() {
    s.ch <- struct{}{}
}

func (s *Semaphore) Release() {
    <-s.ch
}

func main() {
    sem := NewSemaphore(3) // 限制同时访问的goroutine数量为3
    var wg sync.WaitGroup

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            sem.Acquire()
            fmt.Printf("Goroutine %d acquired semaphore\n", i)
            time.Sleep(2 * time.Second)
            fmt.Printf("Goroutine %d releasing semaphore\n", i)
            sem.Release()
        }(i)
    }

    wg.Wait()
    fmt.Println("All goroutines completed")
}

在这个例子中,我们定义了一个 Semaphore 结构体,并通过带缓冲的通道 (channel) 实现信号量的功能。Acquire 方法向通道发送一个空结构体来请求资源,Release 方法从通道接收一个空结构体来释放资源。

3.2.3 信号量的应用场景

信号量适用于以下场景:

  1. 资源池:限制同时访问资源池的 goroutine 数量,例如数据库连接池。
  2. 并发限制:限制同时执行的并发任务数量,防止系统过载。
  3. 生产者-消费者模式:控制生产者和消费者之间的协调,确保生产者不会过度生产,消费者不会过度消费。

3.2.4 示例代码

以下是一个使用信号量实现并发任务限制的示例:

package main

import (
    "fmt"
    "sync"
    "time"
)

type WorkerPool struct {
    sem *Semaphore
    wg  sync.WaitGroup
}

func NewWorkerPool(limit int) *WorkerPool {
    return &WorkerPool{
        sem: NewSemaphore(limit),
    }
}

func (wp *WorkerPool) AddTask(task func()) {
    wp.wg.Add(1)
    go func() {
        defer wp.wg.Done()
        wp.sem.Acquire()
        defer wp.sem.Release()
        task()
    }()
}

func (wp *WorkerPool) Wait() {
    wp.wg.Wait()
}

func main() {
    pool := NewWorkerPool(3) // 限制同时运行的任务数量为3

    for i := 0; i < 10; i++ {
        taskID := i
        pool.AddTask(func() {
            fmt.Printf("Task %d started\n", taskID)
            time.Sleep(2 * time.Second)
            fmt.Printf("Task %d completed\n", taskID)
        })
    }

    pool.Wait()
    fmt.Println("All tasks completed")
}

在这个示例中,我们定义了一个 WorkerPool 结构体,使用信号量来限制同时运行的任务数量。AddTask 方法向任务池中添加任务,Wait 方法等待所有任务完成。

结论

信号量是一种强大的同步原语,可以用于限制同时访问资源的数量,确保系统的稳定性和性能。通过合理使用信号量,可以有效控制并发访问,避免资源竞争和过载。在 Go 语言中,可以通过 sync 包和 channel 实现信号量的功能。在接下来的章节中,我们将继续探讨其他同步原语和并发编程技巧,帮助您更好地掌握 Go 的并发编程。