Golang数据竟态

news2024/12/23 23:43:00

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

一个数据竟态的case

package main

import (
	"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 int
	go 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 int
	var wg sync.WaitGroup
	wg.Add(1) // 表示有一个任务需要等待,等待任务数+1
	go func() {
		num = 53
		wg.Done() // 完成一个处于等待队列的任务,等待任务-1

		// Done decrements the WaitGroup counter by one.
		// func (wg *WaitGroup) Done() {
		//	wg.Add(-1)
		//}

	}()
	wg.Wait() // 阻塞等待,直到等待队列的任务数为0
	return num
}

2、Channel阻塞等待

与1相似

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

3、Channel通道

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

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

	return ch // 返回chan
}

4、互斥锁

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


// 首先,创建一个结构体,其中包含我们想要返回的值以及一个互斥实例
type NumLock struct {
	val int
	m   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 main

import (
	"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 int
	go func() {
		num = 53
	}()
	time.Sleep(500)
	return num
}

func getNumByWaitGroup() int {
	var num int
	var wg sync.WaitGroup
	wg.Add(1) // 表示有一个任务需要等待,等待任务数+1
	go func() {
		num = 53
		wg.Done() // 完成一个处于等待队列的任务,等待任务-1

		// Done decrements the WaitGroup counter by one.
		// func (wg *WaitGroup) Done() {
		//	wg.Add(-1)
		//}

	}()
	wg.Wait() // 阻塞等待,直到等待队列的任务数为0
	return num
}

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

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

	return ch // 返回chan
}

// 首先,创建一个结构体,其中包含我们想要返回的值以及一个互斥实例
type NumLock struct {
	val int
	m   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
}

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

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

相关文章

「Python|场景案例」如何将多个视频合并成多个子画面并排的单个视频?

本文主要介绍如何将多个视频画面合并到一个视频中&#xff0c;使得合成后的视频画面是原视频的并排画面。 文章目录场景描述准备工作处理步骤源代码处理效果展示场景描述 在某些音视频剪辑的场景下我们希望一个视频画面显示多个子画面&#xff0c;比如&#xff1a; 乐器演奏视…

【MySQL】数据库概述

文章目录1、数据库1.1 数据库作用1.2 数据库的相关概念1.3 数据库与数据库管理系统的关系2、MySQL3、RDBMS与非RDBMS3.1 关系型数据库(RDBMS)3.2 非关系型数据库(非RDBMS)3.2.1 键值型数据库3.2.2 文档型数据库3.2.3 搜索引擎数据库3.2.4 列式数据库3.2.5 图形数据库4、关系型数…

基于微信小程序的短视频管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端框架&#xff1a;VUE 数据库&#xff1a;MySQL5.7 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录 一、项目简介 二…

推荐一个跨平台支持Word, Excel, CSV, Email等30多种格式的操作库

更多开源项目请查看&#xff1a;一个专注推荐.Net开源项目的榜单 在我们日常项目开发中&#xff0c;经常需要解析操作文档&#xff0c;比如Office文档、Email文件、PDF、Xml、图片、Mp3等音频文件&#xff0c;操作Office、PDF文件我们需要用到IFilter&#xff0c;操作文本、图片…

【C++】引用与指针

专栏放在【C知识总结】&#xff0c;会持续更新&#xff0c;期待支持&#x1f339;引用引用的概念在C中&#xff0c;引用的本质其实就是给一个已经存在的变量”起别名“。也就是说&#xff0c;引用与它所引用的对象共用一块空间。&#xff08;同一块空间的多个名字&#xff09;就…

【搭建 mybatis 开发环境】

搭建 mybatis 开发环境 环境准备 创建数据库 创建maven 工程 pom文件&#xff1a;添加打包方式为jar 以及 mybatis 和 mysql 依赖 环境搭建 编写实体类 User.java&#xff1a;属性&#xff0c;生成get 和 set 方法 以及 toString方法 创建 IUserDao 接口&#xff0c;操作…

java进阶—一篇文章搞懂set 集合 及其底层实现

上节我们知道了List 下的两大 子类 ArrayList 跟 linkedList ArrayList 数组结构 查询快&#xff0c;增删慢 LinkedList 链表结构 查询慢&#xff0c;增删快 来看看我们今天的主角: Set Set 是 不可重复的&#xff0c;其底下也有两大子接口&#xff1a; HashSet&#xff1…

我用Python轻松玩转ChatGPT的聊天机器人和AI画画

ChatGPT是由人工智能研究实验室OpenAI在2022年11月30日发布的全新聊天机器人模型&#xff0c;一款人工智能技术驱动的自然语言处理工具。它能够通过学习和理解人类的语言来进行对话&#xff0c;还能根据聊天的上下文进行互动&#xff0c;真正像人类一样来聊天交流&#xff0c;甚…

分享112个图片切换,总有一款适合您

分享112个图片切换&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 112个图片切换下载链接&#xff1a;https://pan.baidu.com/s/1Q6DzF8wIQ9rXwFjydI6_kw?pwdktxr 提取码&#xff1a;kt…

Leetcode.1145 二叉树着色游戏

题目链接 Leetcode.1145 二叉树着色游戏 Rating &#xff1a; 1741 题目描述 有两位极客玩家参与了一场「二叉树着色」的游戏。游戏中&#xff0c;给出二叉树的根节点 root&#xff0c;树上总共有 n个节点&#xff0c;且 n为奇数&#xff0c;其中每个节点上的值从 1到 n各不相…

关于“智能出价“的一些探索和实践

背景在移动数字广告营销的整个链路中&#xff0c;主要有三个主体&#xff0c;分别是媒体、adx和dsp.对于一个adx系统&#xff0c;主要有两个功能&#xff0c;分别是&#xff1a;向下对接媒体&#xff1a;汇聚下游的媒体广告流量向上对接dsp(广告主)&#xff1a;将媒体流量卖给上…

【C语言】看了这篇文章,如果你还不会文件操作的话,我把这篇文章给吃了(doge)

&#x1f6a9;write in front&#x1f6a9; &#x1f50e;大家好&#xff0c;我是謓泽&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f3c5;2021年度博客之星物联网与嵌入式开发TOP5&#xff5…

MyBatis注解CRUD执行流程剖析

MyBatis Study Notes Day03 结果映射ResultMap 引入resultMap–MyBatis中最强大的元素 数据库字段名&#xff1a;&#xff1a; 实体类字段名&#xff1a; public class User {private int id;private String name;private String password;如上所示&#xff0c;当sql的字段…

动态规划详解(1)——基础概念

动态规划是数学、编程中一个重要的算法动态规划&#xff08;Dynamic Programming&#xff0c;DP&#xff09;是运筹学的一个分支&#xff0c;是求解决策过程最优化的过程。20世纪50年代初&#xff0c;美国数学家贝尔曼&#xff08;R.Bellman&#xff09;等人在研究多阶段决策过…

Java——根据身高重建队列

题目链接 leetcode在线oj题——根据身高重建队列 题目描述 假设有打乱顺序的一群人站成一个队列&#xff0c;数组 people 表示队列中一些人的属性&#xff08;不一定按顺序&#xff09;。每个 people[i] [hi, ki] 表示第 i 个人的身高为 hi &#xff0c;前面 正好 有 ki 个…

Spring AOP表达式(execution)规则——排除切点的应用

背景 需要项目原切面的基础上排除一些类中方法。 本篇文章主要介绍了SpringBoot AOP Pointcut切入点表达式&#xff0c;以及如何排除某些类中的方法的方式。 execution(* com.winup.web.controller..*.*(..)) 参数说明 符号含义execution&#xff08;&#xff09;表达式的…

【C++之类和对象】默认成员函数

目录前言一、默认成员函数二、构造函数三、析构函数四、拷贝构造函数五、赋值运算符重载前言 前面我们学习了一些类和对象的基本知识&#xff0c;知道了什么是类&#xff0c;类中包括什么东西&#xff0c;以及能够使用一个类来实例化对象&#xff0c;并且会计算类对象的大小。这…

Java Collection 接口下的 “ List 集合” 与 “ Set 集合 ”

Java Collection接口下的“ List 集合” 与 “ Set 集合 ” 每博一文案 一个人最好的底牌&#xff0c;就这两个字: 靠谱,是最高级的聪明。 师父说&#xff1a;人生一回&#xff0c;道义一场&#xff0c;你对人对事的态度&#xff0c;藏着你一生的福报。 千金难买好人缘&#x…

SpringBoot(三):日志文件

目录一、日志文件1.1 日志文件的作用1.2 Spring Boot内置了日志框架1.3 日志的格式说明1.4 自定义日志打印1.5 日志的持久化1.6 日志的级别1.6.1 日志级别有什么作用1.6.2 日志的级别划分1.6.3 日志级别的设置1.7 使用lombok输出日志1.7.1 lombok的原理1.7.2 lombok其他注解一、…

在JS文件中使用或扩展已有的vue文件

工作中遇到一个给现有项目增加一个超时重新登录的提醒框&#xff08;可在提醒框中直接登录本账户&#xff09;。 由于页面稍微复杂&#xff0c;本人又是脚手架一把梭过来的&#xff0c;对于直接使用 js 来完成一整个复杂还带逻辑的页面稍显吃力&#xff0c;所以决定先写一个 vu…