Go并发安全,锁和原子操作

一. 并发安全

        有时候在Go代码中可能存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。

        1.1 互斥锁

        互斥锁是一种常见的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。使用互斥锁来修复上面的代码:

        使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁。当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒策略是随机的。

        1.2 读写互斥锁

        互斥锁是完全互斥的,当一个goroutine获取到锁时,其它的goroutine得等待锁的释放。但是实际情况下,是读多写少的情况,如果使用互斥锁效率会很低。实际当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的。这种场景下使用读写锁效率会更高一点。读写锁在Go语言中使用Sync包中的RWMutex类型。

        读写锁分为两种:读锁和写锁。当一个goroutine获取到读锁,其它协程也可以获取该读锁,但是获取写锁会等待。当一个goroutine获取到写锁,其它goroutine无论是获取读锁还是写锁都会等待。

        示例:

package mainimport ("fmt""sync""time"
)var (x      intwg     sync.WaitGrouprwlock sync.RWMutexlock sync.Mutex
)func write() {//加互斥锁//lock.Lock()//加写锁rwlock.Lock()x = x + 1time.Sleep(10 * time.Millisecond) //加锁读操作耗时10毫秒//lock.Unlock()                     //释放互斥锁rwlock.Unlock() //释放写锁wg.Done()
}func read() {//加互斥锁//lock.Lock()rwlock.RLock() //加读锁time.Sleep(time.Millisecond)//lock.Unlock() //释放互斥锁rwlock.RUnlock() //释放读锁wg.Done()
}func main() {start := time.Now()for i := 0; i < 10; i++ {wg.Add(1)go write()}for i := 0; i < 1000; i++ {wg.Add(1)go read()}wg.Wait()end := time.Now()fmt.Println(end.Sub(start))}

        注意:读写锁适用于读多写少的情况,如果读和写操作差别不大,读写锁优势发挥不出来。

二. Sync包

        2.1 sync.WaitGroup

        我们直到当主协程结束,不论子协程是否执行完都会结束执行。但是在代码中生硬的使用time.Sleep肯定是不合适的,Go语言可以使用sync.WaitGroup来实现并发任务的同步。

        sync.WaitGroup内部维护着计数器,计数器的值可以增加和减少。例如:当我们启动了N个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器为0时,表示所有并发任务已完成。

package mainimport ("fmt""sync"
)var wg sync.WaitGroupfunc test() {defer wg.Done()fmt.Println("hello world")
}func main() {wg.Add(1)go test()fmt.Println("main goroutine done!")wg.Wait()
}

        需要注意:sync.WaitGroup是一个结构体,传递的时候,为了防止拷贝的开销,最好传递指针。

        2.2 sync.Once

        在编程的很多情况下我们需要确保某些操作在高并发场景下只执行一次,例如:只加载一次配置文件,只关闭一次通道等。

        Go语言中的Sync包提供了一个针对只执行一次场景的解决方案——sync.Once

         注意:Do方法中,如果要执行的函数f需要传递参数,就需要搭配闭包来使用。因为函数f没有参数,闭包获取外面的变量。

  • 加载配置文件示例

        延时一个开销很大的初始化操作,到真正用到它的时候执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序启动耗时,而且很有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。

        多个goroutine并发调Icon函数时不是并发安全的,现代编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上自由的重排访问内存顺序。loadIcon函数可能会被重排成以下结果:

func loadIcond() {icons = make(map[string]image.Image)icons["left"] = loadIcon("left.png")icons["right"] = loadIcon("right.png")icons["up"] = loadIcon("up.png")icons["down"] = loadIcon("down.png")
}

        在这种情况下即使判断了icons不是nil也不意味着变量初始化完成了。因为可能一个协程执行到 icons = make(map[string]image.Image),另外一个协程执行到判断 icons == nil,此时该判断为false,但是icons还没有被赋值。

        考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但是这样做会引发性能问题。

        使用sync.Once改造示例代码

         sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。(有点像C/C++里的单例模式),这样设计就能保证初始化操作的时候是并发安全的,并且初始化操作也不会被执行多次。

        2.3 sync.Map

        Go语言内置的的map不是并发安全的。

        像上面这种情况就需要为map加锁来保证并发的安全性,Go语言的Sync包提供了一个开箱即用的并发安全的map——sync.Map,可以直接使用,不需要像内置map一样使用make函数初始化才能使用。同时sync.Map内置了诸如Store,Load,LoadOrStore,Delete,Range等函数。

三. 原子操作(atomic包) 

        代码中的加锁操作因为涉及内核态的上下文切换会比较耗时,代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法他在用户态就可以完成,因此性能比加锁操作更好。Go语言中原子操作有内置标准库sync/atomic提供。

        可以通过标准库文档查看sync/atomic库中提供了哪些方法。Go语言标准库文档中文版 

  • 示例

        比较原子操作和互斥锁 

package mainimport ("fmt""sync""sync/atomic""time"
)var x int64
var mt sync.Mutex
var mg sync.WaitGroup// 不是并发安全的
func add() {x++mg.Done()
}// 加锁版,开销比较大
func mutexAdd() {mt.Lock()x++mt.Unlock()mg.Done()
}// 原子操作版
func atomicAdd() {atomic.AddInt64(&x, 1)mg.Done()
}func main() {start := time.Now()for i := 0; i < 1000; i++ {//go add() 不是并发安全的go mutexAdd()mg.Add(1)}mg.Wait()end := time.Now()fmt.Println(x)fmt.Println(end.Sub(start))start = time.Now()for i := 0; i < 1000; i++ {go atomicAdd()mg.Add(1)}mg.Wait()end = time.Now()fmt.Println(x)fmt.Println(end.Sub(start))}

         atomic包提供了底层的原子级的操作,对于同步算法的实现很有用。这些函数必须谨慎并保证正确的使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

uboot大致流程总结

文章目录 一、uboot介绍二、uboot的配置编译过程2.1 make xxx_defconfig2.2 make 一、uboot介绍 uboot是一个bootloader&#xff0c;用于在嵌入式设备中引导linux内核启动&#xff0c;在嵌入式设备中常见的组织结构如下&#xff1a; 芯片内部固化代码 -> bootloader -> …

40+ Node.js 常见面试问题 [2024]

今天就开始你的Node.js生涯。在这里&#xff0c;我们探讨了最佳Node.js面试问题和答案&#xff0c;以帮助应届生和经验丰富的候选人获得理想的工作。 Node.js 是许多大公司技术堆栈的重要组成部分&#xff0c;例如 PayPal、Trello、沃尔玛和 NASA。 根据 ZipRecruiter 的数据&…

算法练习|Leetcode49字母异位词分词 ,Leetcode128最长连续序列,Leetcode3无重复字符的最长子串,sql总结

目录 一、Leetcode49字母异位词分词题目描述解题思路方法:哈希总结 二、Leetcode128最长连续序列题目描述解题思路方法:总结 三、Leetcode3无重复字符的最长子串题目描述解题思路方法:双指针法总结sql总结 一、Leetcode49字母异位词分词 题目描述 给你一个字符串数组&#xf…

模板初阶

泛型编程&#xff1a; 泛型编程&#xff1a;编写与类型无关的通用代码&#xff0c;模板是泛型编程的基础 class Test { public:void Swap(int& left, int& right){int tmp left;left right;right tmp;}void Swap(double& left, double& right){double tmp…

AR HUD_VSLAM+显示技术

智能座舱的一个重要技术方向是表达与展示。HUD可以将驾驶相关的信息&#xff0c;如车速、导航等投射到驾驶员的视线上方&#xff0c;避免驾驶员的目光离开前方道路。这种显示方式可以提供关键信息的实时展示&#xff0c;减少驾驶员的分心。 HUD的技术原理就是通过光学系统将信息…

网络工程师----第十一天

OSPF&#xff1a; 对称加密算法&#xff1a; 也称为私钥加密或单密钥算法&#xff0c;是一种加密方式&#xff0c;其中加密和解密使用相同的密钥。这种算法的优点包括加密解密速度快、计算量小&#xff0c;适用于大量数据的加密。然而&#xff0c;它的缺点是密钥的安全性难以保…

入坑 Node.js 1

原文&#xff1a;https://blog.iyatt.com/?p14717 前言 前面刚刚对 Spring Boot 有了个概念&#xff0c;再来学学 Node.js&#xff0c;顺便当学 JavaScript&#xff0c;为后面入前端做准备。 环境 Node.js 20.12.2 官方 API 文档&#xff1a;https://nodejs.org/docs/lat…

前端CSS基础6(CSS列表与表格的相关属性,边框的样式调整)

前端CSS基础6&#xff08;CSS列表与表格的相关属性&#xff0c;边框的样式调整&#xff09; CSS列表相关属性CSS表格相关属性回忆表格边框相关属性单元格边框相关属性回忆单元格的跨行和跨列操作单元格边框的相关属性 CSS列表相关属性 在 CSS 中&#xff0c;列表&#xff08;L…

多元函数泰勒公式(含黑塞矩阵)

一元函数的泰勒公式&#xff1a; 接下来&#xff0c;由一元函数有关知识&#xff0c;我们有: 注意这里的dxn中&#xff0c;应把dx看作一个整体&#xff0c;即一个微小变量的n次方 我们接下来推导微分算子&#xff1a; 接下来&#xff0c;把一元泰勒公式转为微分形式: 对于二元…

arm架构,django4.2.7适配达梦8数据库

【Python相关包版本信息】 Django 4.2.7 django-dmPython 3.1.7 dmPython 2.5.5 【达梦数据库版本】 DM Database Server 64 V8 DB Version: 0x7000c 适配过程中发现的问题如下&#xff1a; 错误一&#xff1a;d…

OWASP发布十大开源软件安全风险及应对指南

​ 最近爆发的XZ后门事件&#xff0c;尽管未酿成Log4j那样的灾难性后果&#xff0c;但它再次敲响了警钟&#xff1a;软件供应链严重依赖开源软件&#xff0c;导致现代数字生态系统极其脆弱。面对层出不穷的安全漏洞&#xff0c;我们需要关注开源软件 (OSS)风险 &#xff0c;改进…

上海鑫吉百数——让制造型食品企业焕发新生机!

随着全球化和互联网的普及&#xff0c;食品行业的竞争也日益激烈。数字化转型有助于企业打破地域限制&#xff0c;拓宽市场渠道&#xff0c;提升品牌影响力和竞争力。在信息化、网络化的时代背景下&#xff0c;数字化转型成为企业适应社会发展的必然选择。消费者对于食品的需求…