Go语言入门之锁的使用 | 豆包MarsCode AI刷题

82 阅读4分钟

Go语言入门之锁的使用

在后端开发中,锁(Lock)是一种保证共享资源安全访问的机制。Go语言内置了多种锁机制,例如互斥锁(Mutex)和读写锁(RWMutex),通过这些工具,可以避免数据互补和大量冲突。本文将带大家入门了解Go语言中锁的使用,包括sync.WaitGroup、sync.Mutex以及sync.RWMutex,并结合实际应用场景和代码示例帮助初学者理解。

一、sync.WaitGroup

sync.WaitGroup 是Go标准库中的一个结构体,主要用于一组goroutine等待完成。它通过内部的计数器跟踪goroutine的执行状态。当计数器变稳态时,WaitGroup会解除阻塞,从而继续执行后续代码。下面是sync.WaitGroup的主要方法:

  • Add(delta int) :将 delta 添加到内部计数器上。delta 可以是正数(增加任务)或负数​​(减少任务)。如果计数器变为零,阻塞的等待调用将会被解除。
  • Done() :减少数量1,相当于执行Add(-1)。通常在goroutine结束时调用。
  • Wait() :阻止调用,直到三分之一。

示例代码:

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    urls := []string{
        "http://www.github.com/",
        "https://www.qiniu.com/",
        "https://www.golangtc.com/",
    }

    for _, url := range urls {
        wg.Add(1)
        go func(url string) {
            defer wg.Done()
            _, err := http.Get(url)
            fmt.Println(url, err)
        }(url)
    }

    wg.Wait()
    fmt.Println("所有任务完成")
}

代码解析:

  • 使用wg.Add(1)为每个 goroutine 增加任务计数。
  • 在 goroutine 内部,使用defer wg.Done()表示任务完成后减少计数。
  • wg.Wait()阻止主goroutine,直到所有任务完成。

实际应用场景: 假设我们在视频流应用中需要获取多个服务的数据,如用户关注状态、视频点赞数、评论数等。通过sync.WaitGroup,我们可以同时向多个服务发起请求,加快响应速度,提高用户体验。

func GetVideoData() {
    var wg sync.WaitGroup
    wg.Add(4)

    go func() {
        defer wg.Done()
        // 获取用户关注状态
    }()
    go func() {
        defer wg.Done()
        // 获取视频点赞数
    }()
    go func() {
        defer wg.Done()
        // 获取用户点赞状态
    }()
    go func() {
        defer wg.Done()
        // 获取视频评论数
    }()

    wg.Wait()
}

二、sync.Mutex(互斥锁)

互斥锁(Mutex) 是 Go 语言最常用的锁之一。它保证在同一时间内只有一个 goroutine 能够访问共享数据,从而防止数据竞争。Mutex 主要用于写操作峰值的场景,避免多个 goroutine同时修改数据引发冲突。

示例代码:

package main

import (
    "fmt"
    "sync"
)

var (
    count      int
    countGuard sync.Mutex
)

func GetCount() int {
    countGuard.Lock()
    defer countGuard.Unlock()
    return count
}

func SetCount(c int) {
    countGuard.Lock()
    count = c
    countGuard.Unlock()
}

func main() {
    SetCount(1)
    fmt.Println(GetCount())
}

代码解析:

  • countGuard.Lock()countGuard.Unlock()分别用于锁定和解锁。
  • defer确保在函数结束时释放锁,以防止死锁。
  • 通过锁的使用,实现对共享变量的安全读写器。

使用场景: 互斥锁适用于写多读的场景,例如计数、数据库更新等需要独占访问的操作。


三、sync.RWMutex(读写锁)

读写锁(RWMutex) 是更高级的锁,允许多个 goroutine 同时读取数据,但只允许一个 goroutine 读取数据。它比互斥锁更高效,特别适用于读多写少的场景。

示例代码:

var (
    count      int
    countGuard sync.RWMutex
)

func GetCount() int {
    countGuard.RLock()
    defer countGuard.RUnlock()
    return count
}

func SetCount(c int) {
    countGuard.Lock()
    count = c
    countGuard.Unlock()
}

代码解析:

  • RLock()用于读取操作,而Lock()用于读取操作。
  • 读锁不会阻止其他读操作,但会阻止写操作,从而提高读操作的并发性能。

使用场景: 在存储、配置读取等场景下,数据读取频率远和写入频率时,使用sync.RWMutex可以有效提高吞吐量。


四、使用锁时需要注意的问题

在使用锁时,初学者需要特别注意以下问题:

  1. 死锁:当两个或goroutine互相等待对方多个释放锁时,会导致死锁,程序将无法继续执行。
  2. 活锁:虽然 goroutine 可以释放锁,但由于原因,多个 goroutine 互相干扰,导致程序一直处于完成状态却无法完成任务。
  3. 饥饿:某些 goroutine 长期无法获取锁,而其他 goroutine 占用了大部分时间,导致饥饿问题。

为了解决这些问题,Go 语言提供了管道(Channel)作为更高级的并发工具,通常推荐在复杂的并发场景下优先使用 Channel,而不是锁。


五、个人理解与学习建议

作为初学者,我认为学习Go语言中的锁定机制需要一步步掌握以下要点:

  1. 理解基本概念:首先理解什么是锁、为什么需要锁,并了解了解锁的类型(Mutex、RWMutex 等)。
  2. 从简单到复杂:初学者可以先学习sync.WaitGroup,然后逐步深入理解MutexRWMutex运用。
  3. 多实践:并发编程不是简单的理论知识,而是需要通过实际编码来理解的。可以尝试实现并发爬虫、并发数据处理等小项目。
  4. 善用 Channel:当锁的使用变得复杂时,考虑使用 Channel 替代锁、居民死锁和其他并发问题。

学习建议

  • 编程难在细节,我们不需追求复杂的应用,先掌握基本操作,将它当作一个工具,先学会用再在使用中掌握细节。
  • 多关注错误情况,例如锁未释放导致的死锁问题。
  • 通过调试和输出日志了解goroutine的执行顺序和并行行为,培养并行编程思维。