Golang数据竟态
创始人
2024-05-23 10:48:47
0

本文以一个简单事例的多种解决方案作为引子,用结构体Demo来总结各种并发读写的情况

一个数据竟态的case

package mainimport ("fmt""testing""time"
)func Test(t *testing.T) {fmt.Print("getNum(): ")for i := 0; i < 10; i++ {fmt.Print(strconv.Itoa(getNum()) + " ")}fmt.Println()}
func getNum() int {var num intgo func() {num = 53}()time.Sleep(500)return num
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yUTBgzir-1675414566701)(assets/IMG_21.png)]

在case中,getNum先声明一个变量num,之后在goRoutine中单读对num进行设置,而此时程序也正从函数中返回num, 因为不知道goRoutine是否完成了对num的修改,所以会导致以下两种结果:

  1. goRoutine先完成对num的修改,最后返回5
  2. 变量num的值从函数返回,结果为默认值0

操作完成的顺序不同,导致最后的输出结果不同,这就是将其称为数据竟态的原因。

检查数据竟态

Go有内置的数据竞争检测器,可以使用它来查看潜在的数据竞争条件。使用它就像-race在普通的Go命令行工具中添加标志一样。

  • 运行时检查: go run -race main.go
  • 构建时检查: go build -race main.go
  • 测试时检查: go test -race main.go

所有避免产生竟态背后的核心原则是防止对同一变量或内存位置同时进行读写访问

解决方案

1、WaitGroup等待

解决数据竟态的最直接方法是阻止读取访问操作直到写操作完成为止。
可以以最少的麻烦解决问题,但必须要保证Add和Done出现次数一致,否则会一致阻塞程序,无限制消耗内存,直至资源耗尽服务宕机

func getNumByWaitGroup() int {var num intvar wg sync.WaitGroupwg.Add(1) // 表示有一个任务需要等待,等待任务数+1go func() {num = 53wg.Done() // 完成一个处于等待队列的任务,等待任务-1// Done decrements the WaitGroup counter by one.// func (wg *WaitGroup) Done() {//	wg.Add(-1)//}}()wg.Wait() // 阻塞等待,直到等待队列的任务数为0return num
}

2、Channel阻塞等待

与1相似

func getNumByChannel() int {var num intch := make(chan struct{}) // 创建一个类型为结构体的channel,并初始化为空go func() {num = 53ch <- struct{}{} // 推送一个空结构体到ch}()<-ch // 使程序处于阻塞状态,直到ch获取到推送的值return num
}

3、Channel通道

获取结果后通过通道推送结果,与前两种方法不同,该方法不会进行任何阻塞。
相反,保留了阻塞调用代码的时机,因此它允许更高级别的功能决定自己的阻塞合并发机制,而不是将getXX功能视为同步功能

func getNumByChan() <-chan int {var num intch := make(chan int) // 创建一个类型为int的channelgo func() {num = 53ch <- num // 推送一个int到ch}()return ch // 返回chan
}

4、互斥锁

上述三种方法解决的是num在写操作完成后才能读取的情况
不管读写顺序如何,只要求它们不能同时发生——> 互斥锁


// 首先,创建一个结构体,其中包含我们想要返回的值以及一个互斥实例
type NumLock struct {val intm   sync.Mutex
}func (num *NumLock) Get() int {// The `Lock` method of the mutex blocks if it is already locked// if not, then it blocks other calls until the `Unlock` method is called// Lock方法// 调用结构体对象的Lock方法将会锁定该对象中的变量;如果没有,将会阻塞其他调用,直到该互斥对象的Unlock方法被调用num.m.Lock()// 直到该方法返回,该实例对象才会被解锁defer num.m.Unlock()// 返回安全类型的实例对象中的值return num.val
}func (num *NumLock) Set(val int) {// 类似于上面的getNum方法,锁定num对象直到写入“num.val”的值完成num.m.Lock()defer num.m.Unlock()num.val = val
}func getNumByLock() int {// 创建一个`NumLock`的示例num := &NumLock{}// 使用“Set”和“Get”来代替常规的复制修改和读取值,这样就可以确保只有在写操作完成时我们才能进行阅读,反之亦然go func() {num.Set(53)}()time.Sleep(500)return num.Get()
}

这里要注意,我们无法保证最后取得的num值
当有多个写入和读取操作混合在一起时,使用Mutex互斥可以保证读写的值与预期结果一致

附上结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OTuDXKB7-1675414566702)(assets/IMG_22.png)]

完整代码:

package mainimport ("fmt""strconv""sync""testing""time"
)func Test(t *testing.T) {fmt.Print("getNum(): ")for i := 0; i < 10; i++ {fmt.Print(strconv.Itoa(getNum()) + " ")}fmt.Println()fmt.Print("getNumByWaitGroup(): ")for i := 0; i < 10; i++ {fmt.Print(strconv.Itoa(getNumByWaitGroup()) + " ")}fmt.Println()fmt.Print("getNumByChannel(): ")for i := 0; i < 10; i++ {fmt.Print(strconv.Itoa(getNumByChannel()) + " ")}fmt.Println()fmt.Print("getNumByChan(): ")for i := 0; i < 10; i++ {fmt.Print(strconv.Itoa(<-getNumByChan()) + " ")}fmt.Println()fmt.Print("getNumByLock(): ")for i := 0; i < 10; i++ {fmt.Print(strconv.Itoa(getNumByLock()) + " ")}fmt.Println()fmt.Print("getFact(): ")fmt.Println(getFact())fmt.Println()
}
func getNum() int {var num intgo func() {num = 53}()time.Sleep(500)return num
}func getNumByWaitGroup() int {var num intvar wg sync.WaitGroupwg.Add(1) // 表示有一个任务需要等待,等待任务数+1go func() {num = 53wg.Done() // 完成一个处于等待队列的任务,等待任务-1// Done decrements the WaitGroup counter by one.// func (wg *WaitGroup) Done() {//	wg.Add(-1)//}}()wg.Wait() // 阻塞等待,直到等待队列的任务数为0return num
}func getNumByChannel() int {var num intch := make(chan struct{}) // 创建一个类型为结构体的channel,并初始化为空go func() {num = 53ch <- struct{}{} // 推送一个空结构体到ch}()<-ch // 使程序处于阻塞状态,直到ch获取到推送的值return num
}func getNumByChan() <-chan int {var num intch := make(chan int) // 创建一个类型为int的channelgo func() {num = 53ch <- num // 推送一个int到ch}()return ch // 返回chan
}// 首先,创建一个结构体,其中包含我们想要返回的值以及一个互斥实例
type NumLock struct {val intm   sync.Mutex
}func (num *NumLock) Get() int {// The `Lock` method of the mutex blocks if it is already locked// if not, then it blocks other calls until the `Unlock` method is called// Lock方法// 调用结构体对象的Lock方法将会锁定该对象中的变量;如果没有,将会阻塞其他调用,直到该互斥对象的Unlock方法被调用num.m.Lock()// 直到该方法返回,该实例对象才会被解锁defer num.m.Unlock()// 返回安全类型的实例对象中的值return num.val
}func (num *NumLock) Set(val int) {// 类似于上面的getNum方法,锁定num对象直到写入“num.val”的值完成num.m.Lock()defer num.m.Unlock()num.val = val
}func getNumByLock() int {// 创建一个`NumLock`的示例num := &NumLock{}// 使用“Set”和“Get”来代替常规的复制修改和读取值,这样就可以确保只有在写操作完成时我们才能进行阅读,反之亦然go func() {num.Set(53)}()time.Sleep(500)return num.Get()
}func getFact() []string {ch := make(chan string)//defer close(ch)res := make([]string, 0)num := &NumLock{}go func() {for i := 10; i > 0; i-- {num.Set(i)ch <- strconv.Itoa(num.Get())}close(ch)}()for i := range ch {res = append(res, i)}return res
}

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...