协程是安全的吗?

2023-11-07

前言

我们都知道,多个线程操作同一个变量,是有线程安全问题的。但是,如果换成是“多个协程操作同一个变量”呢?还会有安全问题吗?

实验环境

Windows 11
Go 1.20.2

过程

先看一段Golang代码示例:

func main() {
	count := 0

	wg := sync.WaitGroup{}
	wg.Add(2)
	// 协程1
	go func() {
		for i := 0; i < 100000; i++ {
			count++
		}
		wg.Done()
	}()
	// 协程2
	go func() {
		for i := 0; i < 100000; i++ {
			count++
		}
		wg.Done()
	}()

	wg.Wait() // 等待上面两个协程都执行完了,再往下执行
	fmt.Println(count)
}

上面代码中,协程1 和 协程2 共同对 count 变量执行 +1 操作,各自循环10万遍。

理论上最后输出count的值应该等于20万,但实际输出的值远小于20万。比如我的输出结果是116346

为什么会这样呢,首先是因为 count++ 不是一个原子性的操作,它实际是由三句代码组成的,等同于:

tmp :=  count
tmp = tmp + 1
count = tmp

是一个“先查询后更新”的操作。

然后,Go语言默认情况下是多线程的,线程数量默认等于CPU的核心数。比如双核CPU,Go就会开启两个线程来运行协程,协程1线程A上执行,协程2线程B上执行,由于是多核CPU,这两个线程是可以并行执行的。因此归根到底,这实际上是一个线程安全的问题,即多个线程操作同一个变量导致的。

疑问1

既然是多线程的原因,那如果改成单线程,是不是就不会有问题了呢?

Go语言刚好有提供这样的配置:runtime.GOMAXPROCS(N),其中N就是你想要设置的线程数量。想改为单线程那么只要将N设置为 1 就好。

更改后的代码例子:

func main() {
	runtime.GOMAXPROCS(1) // 设置为单线程
	count := 0

	wg := sync.WaitGroup{}
	wg.Add(2)
	// 协程1
	go func() {
		for i := 0; i < 100000; i++ {
			count++
		}
		wg.Done()
	}()
	// 协程2
	go func() {
		for i := 0; i < 100000; i++ {
			count++
		}
		wg.Done()
	}()

	wg.Wait() // 等待上面两个协程都执行完了,再往下执行
	fmt.Println(count) // 输出:200000
}

果然改为单线程后,就不存在“线程安全”的问题了,能正确输出count的值了。

注:在实际生产环境中,不建议通过设置为单线程模式来避免此类问题,因为单线程会降低Go执行协程的效率,可以通过其它方式,比如加锁、channel之类的方式来解决。

疑问

改为单线程后,就可以毫无顾虑的使用多协程了吗?答:并不是。

在某些场景下如果不注意还是会有隐患的,比如这段代码:

func main() {
	runtime.GOMAXPROCS(1) // 设置为单线程
	count := 0

	wg := sync.WaitGroup{}
	wg.Add(2)
	// 协程1
	go func() {
		for i := 0; i < 100000; i++ {
			count++
		}
		wg.Done()
	}()
	// 协程2
	go func() {
		fmt.Printf("我是协程2,此时count = %d\n", count)
		for i := 0; i < 100000; i++ {
			tmp := count
			tmp = tmp + 1
			count = tmp
		}
		time.Sleep(time.Second * 2) // 此处会迫使协程2让出执行权
		fmt.Printf("我是协程2,已经执行了10万次 +1 操作,此时count = %d\n", count)
		wg.Done()
	}()

	wg.Wait() // 等待上面两个协程都执行完了,再往下执行
}

上述代码会输出:

我是协程2,此时count = 0
我是协程2,已经执行了10万次 +1 操作,此时count = 200000

协程2 一开始查询到的count值是0,执行了10万次+1后,count值居然变成了20万。

这是因为睡眠语句time.Sleep(time.Second * 2)会迫使协程2让出CPU的使用权,让出CPU使用权后,当前线程会改为去执行协程1,当协程1执行完后或遇到 IO阻塞 时,才会又切回来执行协程2,但切回来后,此时的count值已经被协程1修改过了,所以肯定跟协程2刚开始时查询到的值是不一样的。

除了sleep语句外,当协程遇到IO阻塞时,也会让出CPU使用权

所以在协程执行过程中,如果我们想要确保全局变量不会被其它协程修改,就要给变量加锁。

参阅

https://zhuanlan.zhihu.com/p/40279108
https://segmentfault.com/a/1190000041568839

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

协程是安全的吗? 的相关文章

  • Stata数据处理

    作者 Economicoder 公众号 数据学徒 1 快捷键 Fn F2 描述数据 describe Fn PgUp 搜索先前命令 Ctrl 8 打开 data editor browse Ctrl 9 新建do文档 Ctrl D 执行在d

随机推荐

  • 图片转换js (img对象,file对象,base64,canvas对象),以及图片压缩方式

    首先想一想我们有哪些需求 大多时候我们需要将一个File对象压缩之后再变为File对象传入到远程图片服务器 有时候我们也需要将一个base64字符串压缩之后再变为base64字符串传入到远程数据库 有时候后它还有可能是一块canvas画布
  • 【blog】使用github-pages搭建个人博客

    我的博客 以此博客记录学习过程及相关学习笔记 一 选择模板 1 在Jekyll Themes 或者jekyll sites 选择一个你喜欢的模板直接下载 2 在github新建一个项目 选择一个主题 外链图片转存失败 源站可能有防盗链机制
  • 数据库的模糊查询

    命中率越高 策略越好 数据库的模糊查询 work918 在SQL中 模糊查询可以使用LIKE关键字来实现 LIKE关键字后面可以跟一个模式 其中 表示任意数量的字符 表示一个字符 例如 如果你想在一个名为students的表中查找所有名字以
  • python计算正方形、立方体、圆、球的面积和体积

    usr bin env python encoding UTF 8 import math 正方形的面积 def square mianji x return x x 立方体的表面积 def cube x return xx6 立方体的体积
  • linux系统下部署02-InfluxDB的安装和设置密码

    InfluxDB是一个当下比较流行的时序数据库 InfluxDB使用 Go 语言编写 无需外部依赖 安装配置非常方便 适合构建大型分布式系统的监控系统 一 InfluxDB 简介 InfluxDB 是用Go语言编写的一个开源分布式时序 事件
  • 使用高效代理抓取58同城巴州二手房信息并保存至excel

    声明 此程序旨在技术学习交流 促进网络安全 不作任何商业用途 违者责任自负 此程序就是使用代理IP来反爬的一个小案例 使用的高效代理 通过API每次请求提取一个代理IP 一个代理IP 必须是高匿代理 隐藏真实IP 相当于一台主机 只要主机足
  • 无需解密代码!软件保护专家VMProtect 2020全新升级!更丰富的保护功能

    VMProtect是新一代的软件保护实用程序 具有内置的反汇编程序 可与Windows和Mac OS X可执行程序配合使用 还可以链接编译器创建的MAP文件 以快速选择代码片段进行保护 VMProtect的基本原则是通过使应用程序代码和逻辑
  • ReactNative——导航器react-navigation(堆栈式导航器篇)

    react navigation 安装核心包 yarn add react navigation native 安装 react navigation native本身依赖的相关包 react native reanimated 动画库 r
  • MVC中前台Model转Json传到后台

    C 代码 string str Newtonsoft Json JsonConvert SerializeObject Model JS代码 var theString str theString theString replace quo
  • C语言实验——求两个整数之和

    C语言实验 求两个整数之和 C语言实验 求两个整数之和 求两个整数之和 不从键盘输入数据 直接使用赋值语句 a 123 b 456 输入数据 然后计算两个整数之和输出 Input 无输入数据 Output 输出a和b之和 Sample Ou
  • 5.Java中的基本数据类型有哪些?

    Java中的基本数据类型有哪些 Java是一个强类型语言 Java中的数据必须明确数据类型 在Java中的数据类型包括基本数据类型和引用数据类型两种 Java中的基本数据类型 数据类型 关键字 内存占用 成员变量初始值 取值范围 整数类型
  • Coursera

    该系列仅在原课程基础上部分知识点添加个人学习笔记 或相关推导补充等 如有错误 还请批评指教 在学习了 Andrew Ng 课程的基础上 为了更方便的查阅复习 将其整理成文字 因本人一直在学习英语 所以该系列以英文为主 同时也建议读者以英文为
  • Qt 判断QString是否为空

    isEmpty QString isEmpty returns true QString isEmpty returns true QString x isEmpty returns false QString abc isEmpty re
  • Linux 存储结构

    软硬链接 windows中的快捷方式 ln 参数 目标 参数 使用 s s表示创建软链接 默认创建的是硬链接 f 强制创建文件或目录的链接 i 覆盖先询问 v 显示创建过程 echo hello wolrd gt readme txt 创建
  • 相关性分析的五种方法

    相关分析 Analysis of Correlation 是网站分析中经常使用的分析方法之一 通过对不同特征或数据间的关系进行分析 发现业务运营中的关键影响及驱动因素 并对业务的发展进行预测 本篇文章将介绍5种常用的分析方法 在开始介绍相关
  • 【H.264/AVC视频编解码技术详解】十二、解析H.264码流的宏块结构(下):H.264帧内编码宏块的预测结构

    H 264 AVC视频编解码技术详解 视频教程已经在 CSDN学院 上线 视频中详述了H 264的背景 标准协议和实现 并通过一个实战工程的形式对H 264的标准进行解析和实现 欢迎观看 纸上得来终觉浅 绝知此事要躬行 只有自己按照标准文档
  • Docker部署nacos单机版

    Docker部署nacos单机版 1 拉取镜像 获取最新nacos docker pull nacos nacos server 获取指定版本的nacos docker pulll nacos nacos server 1 3 0 2 导入
  • 软件工程概述思维导图总结(二)

    软件工程之软件过程 关于作者 作者介绍 博客主页 作者主页 简介 JAVA领域优质创作者 一名在校大三学生 在校期间参加各种省赛 国赛 斩获一系列荣誉 关注我 关注我学习资料 文档下载统统都有 每日定时更新文章 励志做一名JAVA资深程序猿
  • 使用Python分析股价波动周期

    基本思路是获取股价收盘信息后 使用希尔伯特黄变换将股价波动数据拆解为不同周期的波动曲线 再本别利用频谱分析计算每一个曲线的频率 目标是将股价波动数据拆解为不同周期波动的叠加态 1 获取收盘价 富途有很好的API接口 给我这种小散送了每个月的
  • 协程是安全的吗?

    前言 我们都知道 多个线程操作同一个变量 是有线程安全问题的 但是 如果换成是 多个协程操作同一个变量 呢 还会有安全问题吗 实验环境 Windows 11 Go 1 20 2 过程 先看一段Golang代码示例 func main cou