数据丢失了?我的一百万数据剩下了三十多万,为什么?【并发问题】

博主介绍:

我是了凡,喜欢每日在简书上投稿日更的读书感悟笔名:三月_刘超。专注于 Go Web 后端,了解过一些Python、Java、算法、前端等领域。微信公众号【了凡银河系】期待你的关注,企鹅群号(798829931)。未来大家一起加油啊~


背景

都熟知Go语言擅长并发操作,但是对于初步踏入小白的我,今天遇到了一个问题,就是数据丢失了!!



原问题

今日我写了一个小程序进行尝试并发操作,例如:我们使用了10个协程(goroutine),并发的对一个变量进行累加操作,每个goroutine负责执行十万次操作,我们期望最终结果一定是 十个协程 * 十万次累加 = 一百万条数据,下面我们看一下代码描述的是否正确。

程序

package main

import (
   "fmt"
   "sync"
)

func main() {
    
    
   var count = 0
   // 使用WaitGroup等待10个goroutine完成
   var wg sync.WaitGroup
   wg.Add(10)
   for i := 0; i < 10; i++ {
    
    
      go func() {
    
    
         defer wg.Done()
         // 对变量count执行10次加1
         for j := 0; j < 100000; j++ {
    
    
            count ++
         }
      }()
   }
   // 等待10个goroutine完成
   wg.Wait()
   fmt.Println(count)
}

结果

在这里插入图片描述
我们可以清楚的看到只有 三十多万 的数据没有达到我们期望的数据,为什么呢?

分析问题

首先我们对于下面代码进行分析

go func() {
    
    
 defer wg.Done()
   // 对变量count执行10次加1
   for j := 0; j < 100000; j++ {
    
    
      count ++
   }
}()

其他都没有什么问题,我们细看 for 循环内的操作,只有一个 count ++ 操作,如果有一点语言语法基础的同学应该明白 ++ 操作在计算机底层是如何操作的,至少需要三步以上的操作例如:取变量count的当前值,对这个值加1,把结果在保存到count中(详细在下方),这样很显然不是原子操作,既然是原子一定是不可再分割的,所以这样就可能存在并发的问题。

汇编语言里的 count操作

MOVQ  "".COUNT(SB), AX
LEAQ  1(AX), CX
MOVQ  CX, "".count(SB)

比如,10个goroutine同时读取到 count 的值为9000,接着各自按照自己的逻辑加1,值变成了9001,然后把这个结果再写回到 count 变量。但是,实际上,此时我们增加的总数应该是10才对,这里却只增加了1,好多计数都被“吞”掉了。这是并发访问共享数据常见错误

也许这样的问题一些有经验的很容易就能解决,但是如果一些问题没有经验该如何呢?

如何查找类似问题

这个时候介绍 Go 提供了一个检测并发访问共享资源是否有问题的工具:race detector,它可以自动发现程序有没有data race的问题。

使用检测工具:

go run -race main.go

例如:
在这里插入图片描述
在编译(compile),测试(test)或者运行(run)Go代码的时候,加上race参数,就有可能发现并发问题。以上结果我们可以清晰的看到具体哪一行有并发问题,就很清晰的可以解决了。当然这个工具开启了race的程序部署在线上,还是比较影响性能的。

解决问题

上面的程序问题就是,共享资源是count变量临界区是count ++,只要在临界区前面获取锁,在离开临界区的时候释放锁,就能完美地解决data race问题了。

package main

import (
   "fmt"
   "sync"
)

func main() {
    
    
   // 互斥锁保护计算器
   var mu sync.Mutex

   // 计数器的值
   var count = 0

   // 使用WaitGroup等待10个goroutine完成
   var wg sync.WaitGroup

   wg.Add(10)

   for i := 0; i < 10; i++ {
    
    
      go func() {
    
    
         defer wg.Done()
         // 对变量count执行10次加1
         for j := 0; j < 100000; j++ {
    
    
            mu.Lock()
            count ++
            mu.Unlock()
         }
      }()
   }

   // 等待10个goroutine完成
   wg.Wait()
   fmt.Println(count)
}

结果

在这里插入图片描述
可以发现正常执行,加上data race也没有警告问题,完美解决。

总结

  1. 并发访问共享数据的常见错误的解决方案
  2. 使用 race detectorgo run -race main.go )可以检查并发问题

这次就先讲到这里,如果想要了解更多的golang语言内容一键三连后序每周持续更新!微信公众号【了凡银河系】期待你的关注。


猜你喜欢

转载自blog.csdn.net/weixin_45765795/article/details/120688929