Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Go错误集锦 | 通过示例理解数据竞争及竞争条件

Go错误集锦 | 通过示例理解数据竞争及竞争条件

作者头像
Go学堂
发布于 2023-01-31 07:58:25
发布于 2023-01-31 07:58:25
38300
代码可运行
举报
文章被收录于专栏:Go工具箱Go工具箱
运行总次数:0
代码可运行

大家好,我是渔夫子。今天跟大家聊聊Go并发中的两个重要的概念:数据竞争(data race)和竞争条件(race condition)。

在并发程序中,竞争问题可能是程序面临的最难也是最不容易发现的错误之一。作为Go研发人员,必须要理解竞争的关键特性,例如数据竞争以及竞争条件。下面我们就来看下数据竞争和竞争条件(也称为资源竞争)各自的特性,然后看看各自在何时会产生。

数据竞争(data race)

当两个或多个协程同时访问同一个内存地址,并且至少有一个是在写时,就会发生数据竞争。下面是两个协程对同一个共享变量进行+1操作的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
i := 0
go func() {
    i++
}()

go func() {
    i++
}()

我们运行go run -race main.go,会输出如下提示表明发生了数据竞争:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
==================
WARNING: DATA RACE
Write at 0x00c00008e000 by goroutine 7:
 main.main.func2()
Previous write at 0x00c00008e000 by goroutine 6:
 main.main.func1()
==================

同时,i 的值也是不可预知的。可能是1,也可能是2。

这段代码的问题在哪里呢?实际上i++是三个操作的组合:

  • i中读取值value
  • 将value的值+1
  • 将值写回到 i

场景一:Goroutine1在Goroutine2之前运行完成

在这种场景下,情况将会是如下这样:

Goroutine1

Goroutine2

i 值

初始值

0

读取i的值value

0

将value值+1

0

将值写回到i

1

读取i的值value

1

将value值+1

1

将值写回到i

2

第一个协程读取i的值,然后将值进行+1操作,最后将值写回给i。然后第二个协程再开始执行。因此,i的结果是2.

但是,在上面的示例中,并没有任何机制来保证协程一 一定是在协程二读之前完成的。我们再来看接下来并发的场景。

场景二:Goroutine1和Goroutine2并发执行

在这种场景下,情况将会是如下这样:

Goroutine1

Goroutine2

i

0

读取i的值value

0

读取i的值value

0

将value值+1

0

将value值+1

0

将值写回到i

1

将值写回到i

1

首先,两个协程都从i中读取,得到结果都是0。然后,都将读到的值+1,然后将各自的值写回给i,结果是1。这是不符合我们预期的。

这是数据竞争造成的影响。如果两个协程同时访问同一块内存,并且至少有一个协程写入,就会导致一个不可预期的结果。

如何避免数据竞争的发生?

第一种解决方案是让i++变成原子操作。如下:

Goroutine1

Goroutine2

i

0

读取值并+1操作

1

读取值并+1操作

2

使用这种方式,即使是协程2在协程1之前完成,最终结果也是2。

在Go中,原子操作可以使用atomic包。下面是一个具体使用的示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var i int64
go func() {
    atomic.AddInt64(&i, 1)
}()
go func() {
    atomic.AddInt64(&i, 1)
}()

两个协程对i的操作都是原子性的。一个原子操作是不能被中断。因此,可以避免多个线程在同一时间访问同一共享数据。无论协程的执行顺序如何,i的最终结果都是2。

第二种解决方案是使用同步原语mutex。mutex表示互斥,它确保最多一个goroutine访问所谓的关键部分。在Go中,sync包提供了Mutex类型:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
i := 0
mutex := sync.Mutex{}
go func() {
    mutex.Lock()
    i++
    mutex.Unlock()
}()

go func() {
    mutex.Lock()
    i++
    mutex.Unlock()
}()

在该示例中,对i进行+1操作是关键部分。无论协程的顺序如何,该示例中的i都会有一个确定的输出:2。

哪种方法好呢?首先,atomic包只能操作特定的类型(例如int32,int64等整数)。如果我们有一些其他类型的操作(比如,切片,map以及结构体),我们就不能依赖atomic包来解决问题了。

另一种避免同时读取同一块内存的方法是使用通道在多协程间进行通信。例如,我们可以创建一个channel,然后每个协程将要增加的值输入到通道中,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
i := 0
ch := make(chan int)
go func() {
    ch <- 1
}()

go func() {
    ch <- 1
}()

i += <-ch
i += <-ch

该示例中,每个协程都将增量值(这里是1)依次输入到通道中。父协程管理通道并从通道中读取中对i进行算数加操作。因为只有一个协程在对i进行写操作,所以这种方法不存在数据竞争。

我们对上面做个小结。当多个协程同时访问同一块内存区域时,并且存在至少一个协程在进行写操作时,就会发生数据竞争(data-race)。

我们共演示了3种避免数据竞争的方法:

  • 使用原子操作
  • 使用mutex对同一区域进行互斥操作
  • 使用通道进行通信以保证仅且只有一个协程在进行写操作

在这3种方法中,无论协程的顺序的执行如何,i的值都会是2。

那么,如果一个应用中没有数据竞争的存在,那么是否意味着一定能输出一个确定的结果呢?

竞争条件(race condition)

我们先看一个示例。该示例中在两个协程中对变量i都进行直接赋值操作。我们使用mutex来避免数据竞争:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
i := 0
mutex := sync.Mutex{}
go func() {
    mutex.Lock()
    defer mutex.Unlock()
    i = 1
}()

go func() {
    mutex.Lock()
    defer mutex.Unlock()
    i = 2
}()

第一个协程把1赋给i,第二个协程把2赋给i

在该示例中会产生数据竞争吗?当然不会。两个协程虽然访问同一个变量,但由于我们使用了mutex机制,在同一时间只有一个协程能进行操作。那么,该示例的输出结果是确定的吗?当然不是确定。

变量i的结果依赖于协程的执行顺序,可能是1也可能是2。该示例不会产生数据竞争。但是,存在竞争条件(race condition),也称为资源竞争。当程序的行为依赖于执行顺序或事件发生的时机不可控时就会发生竞争条件。

在该示例中,事件发生的时机就是协程执行的顺序。

保证协程间的执行顺序是协调和编排问题。如果要确保状态从0到1,然后再从1到2,我们就需要找到一种保证协程按序执行的方式。一种方式就是使用通道来解决该问题。此外,如果我们使用了通道进行协调和编排,也可以保证在同一时间只有一个协程在访问公共的部分。这也就意味着我们可以移除mutex。

总结

当我们研发并发程序时,一定要理解数据竞争和竞争条件之间的不同。

数据竞争(data race)的发生条件是:当多个协程同时访问一个相同内存位置,并且至少有一个在进行写入操作时。数据竞争意味着不确定的行为。

然而不存在数据竞争不代表结果就是确定的。实际上,一个应用程序即使不存在数据竞争,但它的行为可能依赖于不可控的发生时间或执行顺序,这就是竞争条件(race condition)。

了解这两个方面对于熟练设计并发应用程序至关重要。


欢迎关注「Go学堂」,让知识活起来

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Go学堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Go语言中常见100问题-#58 Not understanding race problems
竞态问题可能是程序员面临的最困难最隐蔽的问题之一,作为一名Go开发人员,我们必须要了解数据竞争和竞争条件的关键点,出现了会产生什么影响以及如何避免。本节内容将首先讨论数据竞争问题以及竞争的条件,然后将深入研究Go内存模型,并分析它的重要性。
数据小冰
2022/08/15
3990
Go语言中常见100问题-#58 Not understanding race problems
避坑:Go并发编程时,如何避免发生竞态条件和数据竞争
现在,我们已经知道了。在编写并发程序时,如果不谨慎,没有考虑清楚共享资源的访问方式和同步机制,那么就会发生竞态条件和数据竞争这些问题,那么如何避免踩坑?避免发生竞态条件和数据竞争的办法有哪些?请看下面:
不背锅运维
2023/04/25
9670
避坑:Go并发编程时,如何避免发生竞态条件和数据竞争
Golang并发编程控制
重学编程之Golang的plan中的上一篇文章我向大家介绍了,并发编程基础,goroutine的创建,channel,正由于go语言的简洁性,我们可以简易快速的创建任意个协程。同时也留下了许多隐患,如果没有更加深入的学习,其实很难直接将其运用到实际项目中,实际生活中。为什么呢?并发的场景许许多多,但一味的只知道其创建,是很难有效的解决问题。例如以下场景-资源竞争
PayneWu
2020/12/18
5640
Golang并发编程控制
Go通关10:并发控制,同步原语 sync 包
您诸位好啊,我是无尘。又到了愉快的周末,肝了一上午,给大家介绍下 sync 包。除了上一节我们介绍的 channel 通道,还有 sync.Mutex、sync.WaitGroup 这些原始的同步机制,来更加灵活的实现数据同步和控制并发。
微客鸟窝
2021/08/18
5600
理解真实项目中的 Go 并发 Bug
本文内容源于论文《Understanding Real-World Concurrency Bugs in Go》,从 6 个非常流行的开源项目中,收集了 171 个并发 bug,从传统的共享内存访问、Go 语言新的并发原语的特性方面入手,研究了并发 bug 产生的原因以及修复的方法,以便使 Go 研发人员更好的理解 Go 并发模型以及使用 Go 语言编写出更稳定、健壮的软件系统。
Go学堂
2023/01/31
4650
盘点Golang并发那些事儿之二
上一节提到,golang中直接使用关键字go创建goroutine,无法满足我们的需求。主要问题如下
PayneWu
2021/06/10
4940
盘点Golang并发那些事儿之二
Golang异步编程方式和技巧
Golang基于多线程、协程实现,与生俱来适合异步编程,当我们遇到那种需要批量处理且耗时的操作时,传统的线性执行就显得吃力,这时就会想到异步并行处理。下面介绍一些异步编程方式和技巧。
用户2132290
2024/04/12
1.1K0
Golang异步编程方式和技巧
Go by Example 中文版: 互斥锁
在前面的例子中,我们看到了如何使用原子操作来管理简单的计数器。 对于更加复杂的情况,我们可以使用一个互斥锁 来在 Go 协程间安全的访问数据。 示例代码如下:
ccf19881030
2020/09/01
3740
Go by Example 中文版: 互斥锁
GO、Rust这些新一代高并发编程语言为何都极其讨厌共享内存?
今天我想再来讨论一下高并发的问题,我们看到最近以Rust、Go为代表的云原生、Serverless时代的语言,在设计高并发编程模式时往往都会首推管道机制,传统意义上并发控制的利器如互斥体或者信号量都不是太推荐。
beyondma
2021/07/31
6240
go的并发小知识
从第3点钟的操作状态表中可以看到,我们有四种操作会导致goroutine阻塞,三种操作会导致程序panic!因此,为了尽可能转移这些风险,我们需要分配channel的所有权。即,channel的所有者做实例化、写入和关闭操作;channel的使用者做读取操作,且约束其他人无法对其做相应的操作。一个优雅的实现:
天地一小儒
2022/12/28
2210
go的并发小知识
我在使用 Go 过程中犯过的低级错误
循环迭代器变量是一个在每次循环迭代中采用不同值的单个变量。如果我们一直使用一个变量,可能会导致不可预知的行为。
用户5166556
2023/03/18
2.1K0
我在使用 Go 过程中犯过的低级错误
Go并发编程-并发编程难在哪里
编写正确的程序本身就不容易,编写正确的并发程序更是难中之难,那么并发编程究竟难道哪里那?本节我们就来一探究竟。
加多
2020/02/18
6950
Go并发编程-并发编程难在哪里
Go中sync.WaitGroup处理协程同步
一个 sync.WaitGroup 对象可以等待一组协程结束。它很好地解决了 goroutine 同步的问题。
:Darwin
2023/08/11
3920
【初识Go】| Day13 并发编程
“Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.” — Rob Pike
yussuy
2020/12/26
4270
【初识Go】| Day13 并发编程
浅谈Go并发原语
在操作系统中,往往设计一些完成特定功能的、不可中断的过程,这些不可中断的过程称为原语。
闫同学
2024/02/12
3770
GO系列(4)-goroutine基本用法
runtime 调度器是个非常有用的东西,关于 runtime 包几个方法:
爽朗地狮子
2022/10/20
2920
协程锁
我们对一个变量total 进行1000次 +1 操作,不过我们是在多个协程中进行的,猜猜结果如何,我们运行五次看结果
酷走天涯
2019/06/11
5680
协程锁
手摸手Go 深入理解sync.Cond
sync.Cond实现了一个条件变量,用于等待一个或一组goroutines满足条件后唤醒的场景。每个Cond关联一个Locker通常是一个*Mutex或RWMutex`根据需求初始化不同的锁。
用户3904122
2022/06/29
2650
手摸手Go 深入理解sync.Cond
Go 语言互斥锁
在并发编程中,互斥锁(Mutex,全称 Mutual Exclusion)是一个重要的同步原语,用于确保多个线程或进程在访问共享资源时不会发生竞态条件。竞态条件是指在多个线程同时访问或修改共享数据时,由于操作顺序的不确定性,导致数据不一致或者程序行为不可预测的问题。
FunTester
2025/02/19
730
Go 语言互斥锁
Go 专栏|并发编程:goroutine,channel 和 sync
原文链接: Go 专栏|并发编程:goroutine,channel 和 sync
AlwaysBeta
2021/09/16
6640
Go 专栏|并发编程:goroutine,channel 和 sync
相关推荐
Go语言中常见100问题-#58 Not understanding race problems
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验