Go语言中的sync包:同步原语详解

Go语言中的sync包:同步原语详解

大家好,欢迎来到今天的Go语言技术讲座!今天我们要聊的是Go语言中一个非常重要的工具——sync包。如果你是一个喜欢写并发程序的开发者,那么这个包简直就是你的“救星”。它就像是一位严格的交通警察,确保你的协程(goroutines)在繁忙的道路上井然有序地运行。

为了让这次讲座更加轻松愉快,我会用一些诙谐的语言和实际的例子来解释这些概念。别担心,代码会很多,表格也会有,让你看得更清楚明白!


什么是sync包?

首先,我们来简单介绍一下sync包。sync包是Go语言标准库的一部分,提供了许多用于实现线程安全(或者说是协程安全)的工具。它的主要目的是帮助我们在多协程环境下管理共享资源,避免出现竞态条件(race condition)或其他并发问题。

换句话说,sync包就是用来让多个协程“和平共处”的工具集合。


同步原语有哪些?

接下来,我们来看看sync包中提供的几种常见的同步原语:

  1. Mutex(互斥锁)
  2. RWMutex(读写锁)
  3. WaitGroup(等待组)
  4. Once(一次性执行)
  5. Cond(条件变量)

下面我们将逐一讲解这些原语,并通过代码示例说明它们的作用。


1. Mutex(互斥锁)

Mutex是最基本的同步工具之一,用于保护共享资源,防止多个协程同时访问。它的工作原理很简单:如果一个协程持有锁,其他协程就必须等待,直到锁被释放。

示例代码

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    count := 0

    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock() // 加锁
            count++
            fmt.Println("Current count:", count)
            mu.Unlock() // 解锁
        }()
    }
    wg.Wait()
    fmt.Println("Final count:", count)
}

运行结果

每次运行的结果都应该是:

Final count: 10

注意事项

  • 如果忘记调用Unlock(),会导致死锁。
  • Lock()Unlock()必须成对使用。

2. RWMutex(读写锁)

RWMutexMutex的升级版,允许多个协程同时读取共享资源,但只允许一个协程写入。这在读操作远多于写操作的场景下非常有用。

示例代码

package main

import (
    "fmt"
    "sync"
)

func main() {
    var rwMu sync.RWMutex
    data := make(map[string]string)

    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            rwMu.RLock() // 加读锁
            if value, ok := data["key"]; ok {
                fmt.Println("Read:", value)
            } else {
                fmt.Println("Key not found")
            }
            rwMu.RUnlock() // 解读锁
        }(i)
    }

    wg.Add(1)
    go func() {
        defer wg.Done()
        rwMu.Lock() // 加写锁
        data["key"] = "value"
        fmt.Println("Write: key -> value")
        rwMu.Unlock() // 解写锁
    }()

    wg.Wait()
}

注意事项

  • 写锁会阻塞所有读锁和写锁。
  • 多个读锁可以同时存在。

3. WaitGroup(等待组)

WaitGroup用于等待一组协程完成任务后再继续执行。它非常适合需要协调多个协程的场景。

示例代码

package main

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

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 标记任务完成
    fmt.Printf("Worker %d startedn", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d finishedn", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1) // 增加计数器
        go worker(i, &wg)
    }

    wg.Wait() // 等待所有协程完成
    fmt.Println("All workers have finished")
}

注意事项

  • 必须在启动协程之前调用Add()
  • 使用Done()标记协程完成。

4. Once(一次性执行)

Once确保某个操作只执行一次,即使有多个协程同时尝试执行该操作。

示例代码

package main

import (
    "fmt"
    "sync"
)

func main() {
    var once sync.Once
    value := 0

    for i := 0; i < 5; i++ {
        go func() {
            once.Do(func() {
                value = 42
                fmt.Println("Value initialized to 42")
            })
            fmt.Println("Value read:", value)
        }()
    }

    time.Sleep(time.Second) // 等待协程完成
}

注意事项

  • Do()中的函数只会被执行一次。
  • 即使有多个协程调用Do(),也只会有一个协程执行其中的代码。

5. Cond(条件变量)

Cond用于实现复杂的同步逻辑,允许协程等待某些条件满足后再继续执行。

示例代码

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)

    var ready bool

    go func() {
        mu.Lock()
        for !ready {
            cond.Wait() // 等待条件满足
        }
        fmt.Println("Condition met, continuing...")
        mu.Unlock()
    }()

    mu.Lock()
    ready = true
    cond.Signal() // 通知条件已满足
    mu.Unlock()
}

注意事项

  • 必须与MutexRWMutex配合使用。
  • 使用Signal()通知单个协程,使用Broadcast()通知所有协程。

总结

为了让大家更好地理解这些同步原语的特点,我做了一个简单的对比表:

同步原语 功能描述 场景
Mutex 保护共享资源,防止并发访问 修改共享数据时
RWMutex 支持并发读取,限制写入 读多写少的场景
WaitGroup 等待一组协程完成 协调多个协程
Once 确保某段代码只执行一次 初始化全局变量
Cond 实现复杂的同步逻辑 需要等待条件满足

最后,引用一下Go官方文档的一句话:“Concurrency is not parallelism.” 并发不是并行,但它确实能让我们的程序更高效、更优雅!

希望今天的讲座对你有所帮助!如果有任何问题,欢迎随时提问!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注