iis服务器助手广告广告
返回顶部
首页 > 资讯 > 精选 >GO的锁和原子操作实例分析
  • 886
分享到

GO的锁和原子操作实例分析

2023-07-05 06:07:17 886人浏览 安东尼
摘要

本篇内容介绍了“Go的锁和原子操作实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!锁是什么锁 是用于解决隔离性的一种机制某个协程(线程

本篇内容介绍了“Go和原子操作实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

锁是什么

锁 是用于解决隔离性的一种机制

某个协程(线程)在访问某个资源时先锁住,防止其它协程的访问,等访问完毕解锁后其他协程再来加锁进行访问

在我们生活中,我们应该不会陌生,锁是这样的

本意是指置于可启闭的器物上,以钥匙或暗码开启,引申义是用锁锁住、封闭

生活中用到的锁

上锁基本是为了防止外人进来、防止自己财物被盗

编程语言中的锁

锁的种类更是多种多样,每种锁的加锁开销以及应用场景也不尽相同

锁是用来做什么的

用来控制各个协程的同步,防止资源竞争导致错乱问题

高并发的场景下,如果选对了合适的锁,则会大大提高系统的性能,否则性能会降低。

那么知道各种锁的开销,以及应用场景很有必要

GO中的锁有哪些?

  • 互斥锁

  • 读写锁

我们在编码中会存在多个 goroutine 协程同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态

举一个生活中的例子

生活中最明显的例子就是,大家抢着上厕所,资源有限,只能一个一个的用

举一个编码中的例子

package mainimport ("fmt""sync")// 全局变量var num int64var wg sync.WaitGroupfunc add() {for i := 0; i < 10000000; i++ {num = num + 1}// 协程退出, 记录 -1wg.Done()}func main() {// 启动2个协程,记录 2wg.Add(2)go add()go add()// 等待子协程退出wg.Wait()fmt.Println(num)}

按照上述代码,我们的输出结果应该是 20000000,每一个协程计算 10000000 次,可是实际结果却是

10378923

每一次计算的结果还不一样,出现这个问题的原因就是上述提到的资源竞争

两个 goroutine 协程在访问和修改num变量,会存在2个协程同时对num+1 , 最终num 总共只加了 1 ,而不是 2

这就导致最后的结果与期待的不符,那么我们如何解决呢?

我们当然是用锁控制同步了,保证各自协程在操作临界区资源的时候,先确实是否拿到锁,只有拿到锁了才能进行对临界区资源的修改

先来看看互斥锁

互斥锁

GO的锁和原子操作实例分析

互斥锁的简单理解就像上述我们讲到上厕所的案例一样,同一时间点,只能有一个人在使用其他人只能排队等待

编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性

每个对象都对应于一个可称为互斥锁的标记,这个标记用来保证在任一时刻,只能有一个协程访问该对象。

应用场景

写大于读操作的

它代表的资源就是一个,不管是读者还是写者,只要谁拥有了它,那么其他人就只有等待解锁后

我们来使用互斥锁解决上述的问题

互斥锁 - 解决问题

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个 goroutine 协程可以访问共享资源

Go 中使用到如下 1个知识点来解决

sync包Mutex类型 来实现互斥锁

package mainimport (   "fmt"   "sync")// 全局变量var num int64var wg sync.WaitGroupvar lock sync.Mutexfunc add() {   for i := 0; i < 10000000; i++ {      // 访问资源前  加锁      lock.Lock()      num = num + 1      // 访问资源后  解锁      lock.Unlock()   }   // 协程退出, 记录 -1   wg.Done()}func main() {   // 启动2个协程,记录 2   wg.Add(2)   go add()   go add()   // 等待子协程退出   wg.Wait()   fmt.Println(num)}

执行上述代码,我们能看到,输出的结果与我们预期的一致

20000000

使用互斥锁能够保证同一时间有且只有一个goroutine 协程进入临界区,其他的goroutine则在等待锁

当互斥锁释放后,等待的 goroutine 协程才可以获取锁进入临界区

如何知道哪一个协程是先被唤醒呢?

可是,多个goroutine 协程同时等待一个锁时,如何知道哪一个协程是先被唤醒呢?

互斥锁这里的唤醒的策略是随机的,并不知道到底是先唤醒谁

读写锁

为什么有了互斥锁 ,还要读写锁呢?

很明显就是互斥锁不能满足所有的应用场景,就催生出了读写锁,我们细细道来

互斥锁是完全互斥的,不管协程是读临界区资源还是写临界区资源,都必须要拿到锁,否则就无法操作(这个限制太死了对吗)

可是在我们实际的应用场景下是读多写少

若我们并发的去读取一个资源,且不对资源做任何修改的时候如果也要加锁才能读取数据,是不是就很没有必要呢

这种场景下读写锁就发挥作用了,他就相对灵活了,也很好的解决了读多写少的场景问题

读写锁的种类

  • 读锁

  • 写锁

GO的锁和原子操作实例分析

当一个goroutine 协程获取读锁之后,其他的 goroutine 协程如果是获取读锁会继续获得锁

可如果是获取写锁就必须等待

当一个 goroutine 协程获取写锁之后,其他的goroutine 协程无论是获取读锁还是写锁都会等待

我们先来写一个读写锁的DEMO

Go 中使用到如下 1个知识点来解决

sync包RWMutex类型 来实现读写锁

package mainimport (   "fmt"   "sync"   "time")var (   num    int64   wg     sync.WaitGroup   //lock   sync.Mutex   rwlock sync.RWMutex)func write() {   // 加互斥锁   // lock.Lock()   // 加写锁   rwlock.Lock()   num = num + 1   // 模拟真实写数据消耗的时间   time.Sleep(10 * time.Millisecond)   // 解写锁   rwlock.Unlock()   // 解互斥锁   // lock.Unlock()   // 退出协程前 记录 -1   wg.Done()}func read() {   // 加互斥锁   // lock.Lock()   // 加读锁   rwlock.RLock()   // 模拟真实读取数据消耗的时间   time.Sleep(time.Millisecond)   // 解读锁   rwlock.RUnlock()   // 解互斥锁   // lock.Unlock()   // 退出协程前 记录 -1   wg.Done()}func main() {   // 用于计算时间 消耗   start := time.Now()   // 开5个协程用作 写   for i := 0; i < 5; i++ {      wg.Add(1)      go write()   }   // 开500 个协程,用作读   for i := 0; i < 1000; i++ {      wg.Add(1)      go read()   }   // 等待子协程退出   wg.Wait()   end := time.Now()   // 打印程序消耗的时间   fmt.Println(end.Sub(start))}

我们开5个协程用于写,开1000个协程用于读,使用读写锁加锁,结果耗时 54.4871ms 如下

4871ms

如果我们将上述代码修改成加 互斥锁,运行之后的结果是 1.7750029s 如下

7750029s

是不是结果相差很大呢,对于不同的场景应用不同的锁,对于我们的程序性能影响也是很大,当然上述结果,若读协程,和写协程的个数差距越大,结果就会越悬殊

我们总结一下这一小块的逻辑:

GO的锁和原子操作实例分析

  • 写者是排他性的,一个读写锁同时只能有一个写者或多个读者

  • 不能同时既有读者又有写者

  • 如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。

  • 如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

上述提了自旋锁,我们来简单解释一下,什么是自旋锁

自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,即在标志寄存器中关闭/打开中断标志位,不需要自旋锁)。

简单来说,在并发过程中,若其中一个协程拿不到锁,他会不停的去尝试拿锁,不停的去看能不能拿,而不是阻塞睡眠

自旋锁和互斥锁的区别

互斥锁

当拿不到锁的时候,会阻塞等待,会睡眠,等待锁释放后被唤醒

自旋锁

当拿不到锁的时候,会在原地不停的看能不能拿到锁,所以叫做自旋,他不会阻塞,不会睡眠

如何选择锁

对于 C/C++ 而言

  • 若加锁后的业务操作消耗,大于互斥锁阻塞后切换上下文的消耗 ,那么就选择互斥锁

  • 若加锁后的业务操作消耗,小于互斥锁阻塞后切换上下文的消耗,那么选择自旋锁

对于 GO 而言

  • 若写的频次大大的多余读的频次,那么选择互斥锁

  • 若读的频次大大的多余写的频次,那么选择读写锁

我们都是对自身要求比较高的同学,那么有没有比锁还好用的东西呢

自然是有的,我们来看看原子操作

啥是原子操作

"原子操作(atomic operation)是不需要synchronized",这是多线程编程的老生常谈了。所谓原子操作是指不会被线程调度机制打断的操作

这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。

原子操作的特性:

原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断

上述我们的加锁案例,咱们编码中的加锁操作会涉及内核态的上下文切换会比较耗时、代价比较高

针对基本的数据类型我们还可以使用原子操作来保证并发安全

因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好

不用我们自己写汇编,这里 GO 也提供了原子操作的包,供我们一起来使用 sync/atomic

我们对上述的案例做一个延伸

package mainimport ("fmt""sync""sync/atomic""time")var num int64var l sync.Mutexvar wg sync.WaitGroup// 普通版加函数func add() {num = num + 1wg.Done()}// 互斥锁版加函数func mutexAdd() {l.Lock()num = num + 1l.Unlock()wg.Done()}// 原子操作版加函数func atomicAdd() {atomic.AddInt64(&num, 1)wg.Done()}func main() {// 目的是 记录程序消耗时间start := time.Now()for i := 0; i < 20000; i++ {wg.Add(1)// go add()       // 无锁的  add函数 不是并发安全的// go mutexAdd()  // 互斥锁的 add函数 是并发安全的,因为拿不到互斥锁会阻塞,所以加锁性能开销大go atomicAdd()    // 原子操作的 add函数 是并发安全,性能优于加锁的}// 等待子协程 退出wg.Wait()end := time.Now()fmt.Println(num)// 打印程序消耗时间fmt.Println(end.Sub(start))}

我们使用上述 demo 代码,模拟了3种情况下,程序的耗时以及计算结果对比

不加锁

无锁的 add函数 不是并发安全的

19495
11.9474ms

加互斥锁

互斥锁的 add函数 是并发安全的,因为拿不到互斥锁会阻塞,所以加锁性能开销大

20000
14.9586ms

使用原子操作

原子操作的 add函数 是并发安全,性能优于加锁的

20000
9.9726ms

“GO的锁和原子操作实例分析”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!

--结束END--

本文标题: GO的锁和原子操作实例分析

本文链接: https://www.lsjlt.com/news/349957.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • GO的锁和原子操作实例分析
    本篇内容介绍了“GO的锁和原子操作实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!锁是什么锁 是用于解决隔离性的一种机制某个协程(线程...
    99+
    2023-07-05
  • GO的锁和原子操作的示例详解
    目录GO的锁和原子操作分享锁是什么锁是用来做什么的互斥锁互斥锁 - 解决问题读写锁我们先来写一个读写锁的DEMO自旋锁和互斥锁的区别如何选择锁啥是原子操作总结GO的锁和原子操作分享 ...
    99+
    2023-02-24
    GO锁 原子操作 GO锁 GO 原子操作
  • redis原子操作实例分析
    这篇“redis原子操作实例分析”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“redis原...
    99+
    2024-04-02
  • C#原子操作实例分析
    这篇文章主要讲解了“C#原子操作实例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C#原子操作实例分析”吧!知识点竞争条件当两个或两个以上的线程访问共享数据,并且尝试同时改变它时,就发生...
    99+
    2023-06-29
  • 什么是原子操作?深入浅析go中的原子操作
    在我们前面的一些介绍 sync 包相关的文章中,我们应该也发现了,其中有不少地方使用了原子操作。 比如 sync.WaitGroup、sync.Map 再到 sync.Pool,这些结构体的实现中都有原子操作的身影。 原子操作在并发编程中是...
    99+
    2023-05-14
    原子操作 后端 Go
  • CountDownLatch和Atomic原子操作类源码分析
    本篇内容主要讲解“CountDownLatch和Atomic原子操作类源码分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“CountDownLatch和Atomic原子操作类源码分析”吧!引导...
    99+
    2023-06-29
  • OpenMP创建线程中的锁及原子操作性能分析
    这篇文章主要讲解了“OpenMP创建线程中的锁及原子操作性能分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“OpenMP创建线程中的锁及原子操作性能分析”...
    99+
    2024-04-02
  • Java原子操作类源码分析
    这篇文章主要介绍“Java原子操作类源码分析”,在日常操作中,相信很多人在Java原子操作类源码分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Java原子操作类源码分析”的疑惑有所帮助!接下来,请跟着小编...
    99+
    2023-06-30
  • Java多线程Atomic包操作原子变量与原子类的示例分析
    这篇文章主要介绍Java多线程Atomic包操作原子变量与原子类的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!一、何谓Atomic?Atomic一词跟原子有点关系,后者曾被人认为是最小物质的单位。计算机中的...
    99+
    2023-05-30
    java
  • Go中Waitgroup和锁的示例分析
    这篇文章给大家分享的是有关Go中Waitgroup和锁的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。学 Go 的时候知道 Go 语言支持并发,最简单的方法是通过 go 关键字开启 goroutine 即...
    99+
    2023-06-15
  • go 原子操作的方式及实现原理全面深入解析
    目录什么是原子操作?原子操作的使用场景是什么?原子操作是怎么实现的?x86 LOCK 的时候发生了什么原子操作有什么特征?go 里面有哪些原子操作?增减(Add)比较并交换(Comp...
    99+
    2023-05-16
    go 原子操作方式原理 go 原子操作
  • Go语言原子操作及互斥锁的区别是什么
    本篇内容介绍了“Go语言原子操作及互斥锁的区别是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!原子操作就是不可中断的操作,外界是看不到原...
    99+
    2023-06-22
  • redis分布式锁的实现原理实例分析
    这篇文章主要介绍了redis分布式锁的实现原理实例分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇redis分布式锁的实现原理实例分析文章都会有所收获,下面我们一起来看看吧。首先,为了确保分布式锁可用,我们至...
    99+
    2023-06-29
  • go原子操作的方式及实现原理是什么
    今天小编给大家分享一下go原子操作的方式及实现原理是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。什么是原子操作?原子操...
    99+
    2023-07-06
  • CountDownLatch和Atomic原子操作类源码解析
    目录引导语1、CountDownLatch1.1、await1.2、countDown1.3、示例2、Atomic原子操作类3、总结引导语 本小节和大家一起来看看 CountDown...
    99+
    2024-04-02
  • JavaScript的运算符和操作数实例分析
    这篇文章主要介绍“JavaScript的运算符和操作数实例分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“JavaScript的运算符和操作数实例分析”文章能帮助...
    99+
    2024-04-02
  • SVG DOM操作实例分析
    这篇文章主要讲解了“SVG DOM操作实例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“SVG DOM操作实例分析”吧! HTML页面中的DOM操作 ...
    99+
    2024-04-02
  • Go语言互斥锁与读写锁实例分析
    这篇“Go语言互斥锁与读写锁实例分析”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Go语言互斥锁与读写锁实例分析”文章吧。前...
    99+
    2023-06-29
  • C语言中互斥锁与自旋锁及原子操作使用浅析
    目录互斥锁自旋锁原子操作实操结果互斥锁 临界区资源已经被1个线程占用,另一个线程过来访问临界资源的时候,会被CPU切换线程,不让运行后来的这个线程 适用于 锁住的内容多,(例如红黑数...
    99+
    2023-01-11
    C语言互斥锁 C语言自旋锁 C语言原子操作
  • C#操作Excel实现的实例分析
    C#操作Excel实现的实例分析,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。C#操作Excel是怎么样执行的呢?我们在实际的C#操作Excel开发程序过程中主要会使用到那些方...
    99+
    2023-06-17
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作