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

news2024/10/11 12:14:01

在这里插入图片描述

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

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

🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Python全栈,Golang开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。

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

在这里插入图片描述

文章目录

  • Go语言中的Sync锁
    • 一、互斥锁(Mutex)
      • 1.1 基本用法
      • 1.2 使用sync.WaitGroup等待一组Goroutine完成
      • 1.3 注意事项
    • 二、读写锁(RWMutex)
      • 2.1 基本用法
      • 2.2 注意事项
    • 三、Once(一次执行)
      • 3.1 基本用法
      • 3.2 注意事项
    • 四、总结

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 基本用法

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完成。例如:

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 基本用法

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 基本用法

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安全地访问共享资源,避免数据竞争和其他并发问题。希望本文能够对大家有所帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2204817.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI工程师:AI时代的新岗位

从数量上看&#xff0c;AI工程师的数量可能比机器学习工程师/LLM工程师的数量要多得多。一个人可以非常成功地胜任这个角色&#xff0c;而无需接受任何培训。 —— Andrej Karpathy 仅初创公司的收入就超过 10 亿美元&#xff0c;随着 Gen AI 的早期成功迹象&#xff0c;每家有…

The current Windows user is not valid for executing Rabbitmq scripts

问题描述 在New Configuration或者Join host 配置的时候&#xff0c;执行配置信息报错如下 原因分析 可能的原因有如下两点 Cookie没有正确分配给当前用户这台机器是克隆来的&#xff0c;而且改了机器名 问题解决 要解决这个问题&#xff0c;需要卸载Erlang和RabbitMQ并重新安…

大数据-164 Apache Kylin Cube优化 案例1 定义衍生维度与对比 超详细

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

中国数据库产业图谱(2024)

全图下载地址: 中国数据库产业图谱&#xff08;2024&#xff09;

雾霾下雨天气户外人像街景拍摄Lr后期调色,手机滤镜PS+Lightroom预设下载!

调色详情 在雾霾下雨天气下拍摄的户外人像街景往往具有一种独特的氛围。通过 Lightroom 进行后期调色&#xff0c;可以进一步强化这种氛围&#xff0c;使照片更加富有情感和艺术感。 预设信息 调色风格&#xff1a;灰调风格预设适合类型&#xff1a;人像&#xff0c;雾霾&am…

Xcode报错:Undefined symbols,Linker command failed with exit code1

这种编译报错点击Xcode左侧的小红叉这两行点击没反应&#xff0c;不知道具体报错原因怎么弄&#xff1f; 解决办法&#xff1a; 第一步&#xff1a;点周Xcode左侧工具栏的编译log日志按钮 第二步&#xff1a;第一步点击完Xcode左侧出现了编译历史列表&#xff0c;可以看到有报…

如何使用selenium结合最新版chrome爬虫

如何使用selenium结合最新版chrome爬虫 1、下载chrome及其插件chromedriver-win64 点我下载 [百度网盘] 通过百度网盘分享的文件:chrome爬虫插件 链接:https://pan.baidu.com/s/1kqkblX_ordZsQNYR234bMg 提取码:8888 下载后,解压安装。 2、配置电脑系统环境 我的电脑-…

Cocos 2 使用 webview 嵌入页面,摄像头调用没权限问题

Cocos 2 使用 webview 嵌入页面&#xff0c;摄像头调用没权限问题 嗯&#xff0c;这么说呢&#xff0c;这篇博文看自己的实际需求哈&#xff0c;标题写的可能不是很准确。 我这边呢&#xff0c;是遇到这样一个功能&#xff0c;就是有一个服务&#xff0c;他是的页面呢&#xff…

CentOS7离线安装gcc和gcc-c++(亲测成功)

1.点击下载安装包(下载下来的是gz文件&#xff0c;在linux环境下解压) 提取码&#xff1a;1111 2.进入gcc_rpm目录&#xff0c;执行安装命令 cd gcc_rpm/rpm -ivh *.rpm --nodeps --force3.验证gcc是否安装成功 gcc -v安装gcc-c 1.进入 gcc-c 文件件目录下 cd ../gcc-c/…

高校党费收缴系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;党费收缴管理&#xff0c;论坛信息管理&#xff0c;新闻动态管理&#xff0c;公告管理&#xff0c;基础数据管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;公告&…

【刷题】数组中的逆序对

题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09;. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/submissions/571…

嵌入式硬件设计:从原理到实践

嵌入式硬件设计&#xff1a;从原理到实践 嵌入式硬件设计在物联网、智能设备、工业自动化等领域中扮演着至关重要的角色。随着技术的发展&#xff0c;越来越多的设备依赖于嵌入式系统进行实时控制与数据处理。本文将详细介绍嵌入式硬件设计的各个方面&#xff0c;从设计原理到…

Transformer细节问题

一、Transformer 以下是Transformer的一些重要组成部分和特点&#xff1a; 自注意力机制&#xff08;Self-Attention&#xff09;&#xff1a;这是Transformer的核心概念之一&#xff0c;它使模型能够同时考虑输入序列中的所有位置&#xff0c;而不是像循环神经网络&#xff…

成都睿明智科技有限公司正规吗怎么样?

在数字经济的浪潮中&#xff0c;抖音电商以其独特的内容生态和庞大的用户基础&#xff0c;正逐步成为商家们竞相布局的新蓝海。而在这场电商变革的浪潮中&#xff0c;成都睿明智科技有限公司以其专业的服务和敏锐的市场洞察力&#xff0c;成为了众多商家信赖的合作伙伴&#xf…

QAbstractTableModel只有refresh才能调动data()更新表格数据

void refresh() { beginResetModel(); endResetModel(); } QVariant data(const QModelIndex &index, int role) const

2024长城杯WP

WEB SQLUP 打开题目给了一个登录页面结合名字猜测为SQL注入 查看源码发现有hint提示开发者使用的是模式匹配 所以我尝试使用%来模糊匹配&#xff0c;登陆成功 usernameadmin&password% 进入面板之后发现有一个文件上传功能 尝试上传php文件&#xff0c;结果被waf&#xff0…

c#中多态的实例应用说明

在C#中&#xff0c;多态性是通过继承和实现接口来实现的&#xff0c;允许编写可以使用基类型的代码&#xff0c;然后使用派生类型的特定行为。 一.实例界面显示 二.源码界面显示 //定义的基类abstract class Shape{public abstract int Area();//基类中的抽象方法}//定义矩形的…

获得淘宝商品详情高级版 API 返回值实践流程

淘宝详情API接口的测试流程是一个系统而全面的过程&#xff0c;旨在确保API接口的稳定性和可靠性。以下是详细的测试流程&#xff0c;包括测试前的准备工作、测试步骤、测试后的总结&#xff0c;以及必要的测试工具和注意事项。 一、测试前的准备工作 了解API调用文档&#x…

LabVIEW 成绩统计系统

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

vue3实现登录获取token并自动刷新token进行JWT认证

在《django应用JWT(JSON Web Token)实战》介绍了如何通过django实现JWT&#xff0c;并以一个具体API接口实例的调用来说明JWT如何使用。本文介绍如何通过vue3的前端应用来使用JWT认证调用后端的API接口&#xff0c;实现一下的登录认证获取JWT进行接口认证。 一、账号密码登录获…