【Golang】Go多线程中数据不一致问题解决方案--sync锁机制

✨✨ 欢迎大家来到景天科技苑✨✨

🎈🎈 养成好习惯,先赞后看哦~🎈🎈

🏆 作者简介:景天科技苑

🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。

🏆《博客》:Python全栈,Golang开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。

所属的专栏: Go语言开发零基础到高阶实战
景天的主页: 景天科技苑

文章目录

Go语言中的Sync锁

在Go语言的并发编程中,如何确保多个goroutine安全地访问共享资源是一个关键问题。Go语言提供了sync包,其中包含了多种同步原语,用于解决并发编程中的同步问题。本文将详细介绍sync包中的锁机制,并结合实际案例,帮助读者理解和使用这些锁。

要想解决临界资源安全的问题,很多编程语言的解决方案都是同步。
通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个共享数据,当前goroutine访问完毕, 解锁后,其他的goroutine才 能来访问。

我们可以借助于sync包下的锁操作。 synchronization

但是实际上,在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信:锁,而要以通信的方式去共享内存。

共享内存的方式

锁:多个线程拿的是同一个钥匙,go语言不建议使用锁机制来解决。不要以共享内存的方式去通信

而要以通信的方式去共享内存 go语言更建议我们使用 chan(通道) 来解决安全问题。(后面会学)

在Go语言中并不鼓励用锁保护共享状态的方式,在不同的Goroutine中分享信息(以共享内存的方式去通信)。

而是鼓励通过channeI将共享状态或共享状态的变化在各个Goroutine之间传递(以通信的方式去共享内存),这样同样能像用锁一样保证在同一的时间只有一个Goroutine访问共享状态。

当然,在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。
Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多。

一、互斥锁(Mutex)

互斥锁(sync.Mutex)是最基本的同步机制之一,用于确保同一时间只有一个goroutine能够访问特定的资源。

当一个goroutine持有互斥锁时,其他试图获取该锁的goroutine将会被阻塞,直到锁被释放。

1.1 基本用法

go 复制代码
package main

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

// 定义全局变量 票库存为10张
var tickets int = 10

// 定义一个锁  Mutex 锁头
var mutex sync.Mutex

func main() {
    go saleTicket("张三")
    go saleTicket("李四")
    go saleTicket("王五")
    go saleTicket("赵六")

    time.Sleep(time.Second * 5)
}

// 售票函数
func saleTicket(name string) {
    for {
        // 在拿到共享资源之前先上锁
        mutex.Lock()
        if tickets > 0 {
            time.Sleep(time.Millisecond * 1)
            fmt.Println(name, "剩余票的数量为:", tickets)
            tickets--
        } else {
            // 票卖完,解锁
            mutex.Unlock()
            fmt.Println("票已售完")
            break
        }
        // 操作完毕后,解锁
        mutex.Unlock()
    }
}

上锁之后,就不会出现问题了

1.2 使用sync.WaitGroup等待一组Goroutine完成

sync.WaitGroup类型可以用来等待一组Goroutine完成。例如:

go 复制代码
package main

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

// waitgroup、

var wg sync.WaitGroup

func main() {
    // 公司最后关门的人   0
    // wg.Add(2) wg.Add(2)来告诉WaitGroup我们要等待两个Goroutine完成  开启几个协程,就add几个
    // wg.Done() 我告知我已经结束了  defer wg.Done()来在Goroutine完成时通知WaitGroup
    // 开启几个协程,就add几个
    wg.Add(2)

    go test1()
    go test2()

    fmt.Println("main等待ing")
    wg.Wait() // 等待 wg 归零,wg.Wait()来等待所有Goroutine完成 代码才会继续向下执行
    fmt.Println("end")

    // 理想状态:所有协程执行完毕之后,自动停止。
    //如果每次都强制设置个等待时间。那么协程代码也可能在这个时间内还没跑完,也可能提前就跑完了,所以设置死的等待时间不合理。此时就需要用到了等待组WaitGroup
    //time.Sleep(1 * time.Second)

}
func test1() {
    for i := 0; i < 10; i++ {
        time.Sleep(1 * time.Second)
        fmt.Println("test1--", i)
    }
    wg.Done() //这里就将该代码块放在了其他逻辑之后
}
func test2() {
    defer wg.Done() // defer wg.Done()来在Goroutine完成时通知WaitGroup  如果不用defer就得把该方法放在其他代码之后
    for i := 0; i < 10; i++ {
        fmt.Println("test2--", i)
    }
}

主线程会等待所有协程执行完毕,才继续往下执行代码

1.3 注意事项

避免死锁 :确保在获取锁之后,无论发生什么情况(包括panic),都能够释放锁。可以使用defer语句来确保锁的释放。
减少锁的持有时间 :锁的持有时间越长,其他goroutine被阻塞的时间就越长,系统的并发性能就越差。因此,应该尽量减少锁的持有时间,只在必要的代码段中持有锁。
避免嵌套锁:尽量避免在一个锁已经持有的情况下再尝试获取另一个锁,这可能会导致死锁。

避免忘记调用Done :如果忘记调用Done方法,WaitGroup将会永远等待下去,导致程序无法正常结束。
避免负数计数器:调用Add方法时,如果传入的参数为负数,或者导致计数器变为负数,将会导致panic。

二、读写锁(RWMutex)

读写锁(sync.RWMutex)允许多个goroutine同时读取资源,但在写入时会阻塞所有其他读和写的goroutine。读写锁可以提高读多写少的场景下的并发性能。

2.1 基本用法

go 复制代码
package main

import (
    "fmt"
    "sync"
)

var (
    data map[string]int
    rwMu sync.RWMutex
)

func readData(key string) int {
    rwMu.RLock()
    defer rwMu.RUnlock()
    return data[key]
}

func writeData(key string, value int) {
    rwMu.Lock()
    defer rwMu.Unlock()
    data[key] = value
}

func main() {
    data = make(map[string]int)

    var wg sync.WaitGroup

    // 写操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            writeData(fmt.Sprintf("key%d", i), i*10)
        }(i)
    }

    // 读操作
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            value := readData(fmt.Sprintf("key%d", i%10))
            fmt.Printf("Read: key%d = %d\n", i%10, value)
        }(i)
    }

    wg.Wait()
}

在上述代码中,我们定义了一个全局变量data和一个读写锁rwMu。readData函数用于读取data中的值,在读取之前先获取读锁,读取完成后释放读锁。

writeData函数用于写入data中的值,在写入之前先获取写锁,写入完成后释放写锁。

在main函数中,我们启动了10个写goroutine和100个读goroutine,分别调用writeData和readData函数。通过sync.WaitGroup等待所有goroutine完成。

2.2 注意事项

避免写锁长时间持有:写锁会阻塞所有其他读和写的goroutine,因此应该尽量减少写锁的持有时间。

读多写少场景:读写锁适用于读多写少的场景,如果写操作非常频繁,读写锁的性能优势可能会消失。

避免嵌套锁:与互斥锁类似,读写锁也应该避免嵌套使用。

三、Once(一次执行)

sync.Once用于确保某个操作只执行一次,无论有多少个goroutine调用它。这对于单例模式或初始化只执行一次的场景非常有用。

3.1 基本用法

go 复制代码
package main

import (
    "fmt"
    "sync"
)

var (
    once    sync.Once
    message string
)

func initMessage() {
    message = "Hello, World!"
}

func printMessage() {
    once.Do(initMessage)
    fmt.Println(message)
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            printMessage()
        }()
    }

    wg.Wait()
}

在上述代码中,我们定义了一个全局变量message和一个sync.Once类型的变量once。

initMessage函数用于初始化message的值。printMessage函数通过once.Do方法确保initMessage只被调用一次,然后打印出message的值。

在main函数中,我们启动了10个goroutine,每个goroutine都调用printMessage函数。通过sync.WaitGroup等待所有goroutine完成。

3.2 注意事项

避免重复初始化:sync.Once确保某个操作只执行一次,因此它通常用于初始化全局变量或执行其他只需要执行一次的操作。

性能开销:虽然sync.Once的性能开销很小,但在高性能要求的场景下,仍然需要注意其使用。

四、总结

本文详细介绍了Go语言中sync包中的锁机制,包括互斥锁(sync.Mutex)、读写锁(sync.RWMutex)、Once(一次执行)和WaitGroup(等待组)。

通过实际案例,帮助读者理解和使用这些锁。在并发编程中,正确地使用这些同步原语,可以确保多个goroutine安全地访问共享资源,避免数据竞争和其他并发问题。希望本文能够对大家有所帮助。

相关推荐
鶴哥只手遮天5 分钟前
《Redis 8.10.3 深度剖析》系列 · 第 1 章·全景与源码地图:版本谱系、构建体系、全局状态与启动链路
redis·后端·开源
用户400315043355 分钟前
《从 skynet.timeout 到回调执行:Skynet 分层时间轮源码剖析》
后端
一条Go9 分钟前
四个进程,一把锁:Alioth 的告警评测为什么不需要消息队列
后端·设计模式·架构
用户19961435599620 分钟前
插件是怎么被安全装进系统的
后端
Pniubi34 分钟前
Java为什么要分堆和栈
java·开发语言
愚农搬码1 小时前
自研表单引擎、开源组件二开和商业产品,企业应该如何选择?
后端
用户298698530141 小时前
Python 自动化处理 Word 页眉页脚的几种典型场景
后端·python·api
小小晓.1 小时前
Qt 对话框、QLineEdit 与布局管理实战详解
开发语言·qt
愚农搬码2 小时前
一款企业级电子表单系统应该具备哪些功能?
后端
炸薯条!2 小时前
博客系统测试报告
开发语言·lua