Go互斥锁和读写锁完整代码举例

 更新时间:2026年06月03日 10:35:22   作者:QX_hao  
读写锁(sync.RWMutex)与互斥锁(sync.Mutex)是Go语言中两种最核心的并发控制机制,本质区别在于对读操作的并发支持,这篇文章主要介绍了Go互斥锁和读写锁的相关资料,需要的朋友可以参考下

作用

互斥锁(Mutex)和读写锁(RWMutex)是Go语言中用于保护共享资源、防止数据竞争的重要同步机制。

1. 数据竞争问题

1.1 数据竞争

数据竞争(Data Race)发生在多个goroutine并发访问同一共享变量,且至少有一个访问是写入操作时。如果没有适当的同步机制,会导致不可预测的结果。

1.2 数据竞争的危害

  • 结果不确定性:相同的代码可能产生不同的结果
  • 内存损坏:可能导致程序崩溃或数据损坏
  • 难以调试:问题可能只在特定条件下出现

2. 互斥锁(Mutex)

2.1 互斥锁

互斥锁(Mutual Exclusion Lock)是最基本的同步原语,保证同一时间只有一个goroutine可以访问受保护的临界区。

2.2 互斥锁操作

var mutex sync.Mutex

// 加锁
mutex.Lock()

// 临界区代码
// ...

// 解锁
mutex.Unlock()

3. 代码示例分析

3.1 数据竞争演示

package test_lock

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

var wait sync.WaitGroup
var count1 = 0

func Demo01() {
	wait.Add(10)
	for i := 0; i < 10; i++ {
		go func(data *int) {
			// 模拟访问耗时
			time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
			// 访问数据
			temp := *data
			// 模拟计算耗时
			// 拉大时间,让数据竞争更严重(更好观察结果)
			time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
			ans := 1
			// 修改数据
			*data = temp + ans
			fmt.Println("goroutine", i, "count1:", *data)
			wait.Done()
		}(&count1)
	}
	wait.Wait()
	fmt.Println("最终结果", count1)
}

核心要点:

  1. 无锁并发访问

    var count1 = 0
    
    go func(data *int) {
        temp := *data           // 读取共享变量
        // ... 计算耗时
        *data = temp + ans      // 写入共享变量
    }(&count1)
    
  2. 数据竞争特征

    • 多个goroutine同时读写count1变量
    • 读-改-写操作不是原子的
    • 最终结果通常小于预期值

关键:

  • 典型的数据竞争
  • 由于goroutine执行顺序不确定,结果不可预测
  • 需要同步机制来保证正确性

3.2 互斥锁解决方案

package test_lock

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

var wg sync.WaitGroup
var count = 0

// 声明一个互斥锁
var mutex sync.Mutex

func Demo02() {
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func(data *int) {
			// 加锁
			mutex.Lock()

			// 模拟访问耗时
			time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
			// 访问数据
			temp := *data
			// 模拟计算耗时
			// 拉大时间,让数据竞争更严重(更好观察结果)
			time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
			ans := 1
			// 修改数据
			*data = temp + ans

			// 解锁
			mutex.Unlock()

			fmt.Println("goroutine", i, "count:", *data)
			wg.Done()
		}(&count)
	}
	wg.Wait()
	fmt.Println("最终结果", count)
}

核心要点:

  1. 互斥锁保护临界区

    var mutex sync.Mutex
    
    go func(data *int) {
        // 加锁
        mutex.Lock()
        
        temp := *data
        // ... 计算耗时
        *data = temp + ans
        
        // 解锁
        mutex.Unlock()
    }(&count)
    
  2. 锁的正确使用

    • 在访问共享资源前加锁
    • 在操作完成后立即解锁
    • 使用defer确保解锁被执行

关键:

  • 互斥锁保证了操作的原子性
  • 最终结果总是正确的(10个goroutine,每个+1,结果应为10)
  • 性能会有一定损耗,但保证了正确性

3.3 读写锁(RWMutex)

package test_lock

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

var wg3 sync.WaitGroup
var rw sync.RWMutex

// 实现可以多人读  但是只能一人写
func write(count int) {
	defer wg3.Done()
	rw.Lock()
	defer rw.Unlock()
	fmt.Println("goroutine", count, "写操作>>>>>>>")
	time.Sleep(time.Second * 2)
}

func read(count int) {
	defer wg3.Done()
	fmt.Println("goroutine", count, "<<<<<<<<<读操作")
	time.Sleep(time.Second * 2)
}

func Demo03() {
	for i := 0; i < 10; i++ {
		wg3.Add(1)
		go write(i)
	}

	for i := 0; i < 10; i++ {
		wg3.Add(1)
		go read(i)
	}

	wg3.Wait()
}

核心要点:

  1. 读写锁操作

    var rw sync.RWMutex
    
    // 写锁操作
    func write(count int) {
        rw.Lock()           // 加写锁
        defer rw.Unlock()   // 解读锁
        // 写操作...
    }
    
    // 读锁操作  
    func read(count int) {
        rw.RLock()          // 加读锁
        defer rw.RUnlock()  // 解读锁
        // 读操作...
    }
    
  2. 读写锁特性

    • 读锁:多个goroutine可以同时持有读锁
    • 写锁:写锁是排他的,持有写锁时不能有读锁或其他写锁
    • 升级规则:读锁不能升级为写锁

关键:

  • 读写锁适用于"读多写少"的场景
  • 提高了并发读取的性能
  • 写操作仍然需要独占访问

4. 锁的类型对比

4.1 互斥锁 vs 读写锁

特性互斥锁(Mutex)读写锁(RWMutex)
并发读取不支持支持多个goroutine同时读取
写入操作完全互斥完全互斥
性能开销较低较高(需要维护读锁计数)
适用场景读写频率相当读多写少

4.2 选择原则

使用互斥锁的情况:

  • 读写操作频率相当
  • 临界区代码执行时间短
  • 代码逻辑简单

使用读写锁的情况:

  • 读操作远多于写操作
  • 读操作耗时较长
  • 需要最大化读取并发性

5. 锁的使用场景

5.1 共享数据保护

// 保护共享map
var dataMap = make(map[string]string)
var mapMutex sync.RWMutex

func GetValue(key string) string {
    mapMutex.RLock()
    defer mapMutex.RUnlock()
    return dataMap[key]
}

func SetValue(key, value string) {
    mapMutex.Lock()
    defer mapMutex.Unlock()
    dataMap[key] = value
}

5.2 计数器保护

// 原子计数器
var counter int
var counterMutex sync.Mutex

func Increment() {
    counterMutex.Lock()
    defer counterMutex.Unlock()
    counter++
}

func GetCount() int {
    counterMutex.Lock()
    defer counterMutex.Unlock()
    return counter
}

5.3 资源池管理

// 连接池管理
type ConnectionPool struct {
    pool []*Connection
    mutex sync.Mutex
}

func (p *ConnectionPool) Get() *Connection {
    p.mutex.Lock()
    defer p.mutex.Unlock()
    
    if len(p.pool) > 0 {
        conn := p.pool[0]
        p.pool = p.pool[1:]
        return conn
    }
    return nil
}

6. 指南

6.1 锁的使用

  1. 保持锁的粒度适中

    // 错误:锁的粒度过大
    mutex.Lock()
    // 大量非临界区代码...
    // 共享资源访问
    mutex.Unlock()
    
    // 正确:只保护必要的临界区
    // 非临界区代码...
    mutex.Lock()
    // 共享资源访问
    mutex.Unlock()
    // 非临界区代码...
    
  2. 使用defer确保解锁

    func safeOperation() {
        mutex.Lock()
        defer mutex.Unlock()  // 确保在任何情况下都会解锁
        
        // 可能panic的代码
        if err != nil {
            panic("error")
        }
    }
    

6.2 避免死锁

  1. 锁的顺序一致性

    // 错误:可能产生死锁
    func operation1() {
        mutexA.Lock()
        mutexB.Lock()
        // ...
        mutexB.Unlock()
        mutexA.Unlock()
    }
    
    func operation2() {
        mutexB.Lock()  
        mutexA.Lock()  // 可能死锁
        // ...
    }
    
    // 正确:保持一致的锁顺序
    func operation1() {
        mutexA.Lock()
        mutexB.Lock()
        // ...
    }
    
    func operation2() {
        mutexA.Lock()  // 先获取A锁
        mutexB.Lock()  // 再获取B锁
        // ...
    }
    
  2. 使用超时机制

    // 使用context实现超时
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    select {
    case <-acquireLock(ctx, mutex):
        // 成功获取锁
    case <-ctx.Done():
        // 超时处理
    }
    

6.3 性能优化

  1. 减少锁竞争

    • 使用更细粒度的锁
    • 考虑使用无锁数据结构
    • 使用本地缓存减少锁的使用频率
  2. 读写分离

    // 使用读写锁优化读多写少的场景
    var rw sync.RWMutex
    var data []string
    
    // 多个goroutine可以同时读取
    func ReadData() []string {
        rw.RLock()
        defer rw.RUnlock()
        return data
    }
    
    // 写操作仍然需要互斥
    func WriteData(newData []string) {
        rw.Lock()
        defer rw.Unlock()
        data = newData
    }
    

7. 高级主题

7.1 条件变量(Cond)

条件变量用于在特定条件下等待或通知goroutine:

var mutex sync.Mutex
cond := sync.NewCond(&mutex)

// 等待条件
func waitForCondition() {
    mutex.Lock()
    for !condition {
        cond.Wait()  // 释放锁并等待
    }
    // 条件满足,执行操作
    mutex.Unlock()
}

// 通知条件满足
func signalCondition() {
    mutex.Lock()
    condition = true
    cond.Signal()  // 通知一个等待的goroutine
    mutex.Unlock()
}

7.2 原子操作

对于简单的计数器操作,可以使用原子操作避免锁的开销:

import "sync/atomic"

var counter int64

func Increment() {
    atomic.AddInt64(&counter, 1)
}

func GetCount() int64 {
    return atomic.LoadInt64(&counter)
}

8. 调试和测试

8.1 数据竞争检测

使用Go内置的竞争检测器:

# 编译时启用竞争检测
go build -race main.go

# 运行程序
go run -race main.go

8.2 性能分析

使用pprof分析锁竞争:

import _ "net/http/pprof"

// 在程序中启动pprof服务器
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

总结 

到此这篇关于Go互斥锁和读写锁的文章就介绍到这了,更多相关Go互斥锁和读写锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解Go 结构体格式化输出

    详解Go 结构体格式化输出

    这篇文章主要介绍了Go 结构体格式化输出的相关资料,帮助大家更好的理解和学习go语言,感兴趣的朋友可以了解下
    2020-08-08
  • Go语言学习之goroutine详解

    Go语言学习之goroutine详解

    Goroutine是建立在线程之上的轻量级的抽象。它允许我们以非常低的代价在同一个地址空间中并行地执行多个函数或者方法,这篇文章主要介绍了Go语言学习之goroutine的相关知识,需要的朋友可以参考下
    2020-02-02
  • Go中的格式化字符串fmt.Sprintf()和fmt.Printf()使用示例

    Go中的格式化字符串fmt.Sprintf()和fmt.Printf()使用示例

    这篇文章主要为大家介绍了Go中的格式化字符串fmt.Sprintf()和fmt.Printf()使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-06-06
  • Go Build编译打包文件的完整步骤

    Go Build编译打包文件的完整步骤

    go build命令是用于编译Go语言程序并生成可执行文件,它可以将Go源代码编译成机器代码,并将其打包成可执行文件,方便在不同的操作系统上运行,这篇文章主要给大家介绍了关于Go Build编译打包文件的完整步骤,需要的朋友可以参考下
    2024-02-02
  • Go函数使用(函数定义、函数声明、函数调用等)

    Go函数使用(函数定义、函数声明、函数调用等)

    本文主要介绍了Go函数使用,包括函数定义、函数声明、函数调用、可变参数函数、匿名函数、递归函数、高阶函数等,感兴趣的可以了解一下
    2023-11-11
  • 一文带你了解Go中的内存对齐

    一文带你了解Go中的内存对齐

    一旦涉及到较为底层的编程,特别是与硬件交互,内存对齐是一个必修的课题,所以这篇文章小编就想来和大家聊一聊Go语言中的内存对齐,希望对大家有所帮助
    2023-10-10
  • go语言的sql包原理与用法分析

    go语言的sql包原理与用法分析

    这篇文章主要介绍了go语言的sql包原理与用法,较为详细的分析了Go语言里sql包的结构、相关函数与使用方法,需要的朋友可以参考下
    2016-07-07
  • Go利用ffmpeg进行视频和音频处理

    Go利用ffmpeg进行视频和音频处理

    ffmpeg 是一款功能强大的多媒体处理工具,支持视频和音频的编码、解码、转码,以及帧提取和流处理等功能,下面我们就来看看Go如何利用ffmpeg进行视频和音频处理吧
    2024-12-12
  • go语言制作的zip压缩程序

    go语言制作的zip压缩程序

    这篇文章主要介绍了go语言制作的zip压缩程序,其主体思路是首先创建一个读写缓冲,然后用压缩器包装该缓冲,用Walk方法来将所有目录下的文件写入zip,有需要的小伙伴参考下。
    2015-03-03
  • Go之接口型函数用法

    Go之接口型函数用法

    这篇文章主要介绍了Go之接口型函数用法,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02

最新评论