Golang中Race Condition问题的分析与复现(附play.golang.org演示)

Golang中Race Condition问题的分析与复现(附play.golang.org演示) Todd McCleod 在一节名为“竞态条件”的课程中,使用了这段代码: https://play.golang.org/p/FYGoflKQej 来说明竞态条件不是好的代码。竞态条件会导致不可预测的结果。 我想借助这个主题,按照你们在论坛上教我的方式,逐步理解这段代码以及他所说的内容。 什么是“runtime”,有没有办法让我自己弄清楚? 什么是“sync”,有没有办法让我自己弄清楚?

14 回复

hollowaykeanho: 它是否独立执行而不影响运行器?

有趣的想法

更多关于Golang中Race Condition问题的分析与复现(附play.golang.org演示)的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


以下是一个有趣的定义。

同步 基本上意味着你一次只能执行一件事。异步 意味着你可以同时执行多件事,并且不必等待当前任务完成就可以继续执行下一个任务。

hollowaykeanho:

哇……冷静一下。喝杯冰柠檬汁。用简单的英语就行。没那么复杂。😅

哈哈。你说得对!但这就是我从谷歌得到的答案类型。我很少能读完第一句或第二句。

现在有点跑题了。刚开始学习如何在手机上浏览这个网站。

好的,我又找到了。

hollowaykeanho: 同步还是异步?

第一个外来词的意思是“同时发生”……不是吗?如果我努力回忆遥远的拉丁语课程,下一个词的意思应该相反。

hollowaykeanho: “我”与我的“手表”同步是在什么时候?

是我戴上它的时候吗?或者可能是我使用它的时候?我有一块智能手表,那是我启动健身应用的时候吗?对于老式手表,是我设置时间和日期的时候吗?

异步我就完全没头绪了。

好的,我理解您希望将HTML内容转换为Markdown格式,并遵循严格的规则:准确翻译英文文本、保持代码原样并用指定语言代码块包裹、转换图片链接、移除用户信息、只输出最终结果。

以下是根据您的要求转换后的Markdown文档:

嗯,我以为您希望我关闭所有主题。所以我随机选择了一个解决方案。感谢您的回答。 我大致理解了您的回答。

为了支持第一种方式,Go提供了channel数据类型来进行消息传递。

为了支持第二种方式,Go提供了sync包来管理同步。

这一点,我需要进一步研究。

计算·电信

指一种计算机控制时序协议,其中特定操作在收到前一个操作已完成的指示(信号)后开始。

哇……冷静一下。喝杯冰柠檬汁。用简单的英语解释就行。没那么复杂。 😅

什么类型的事情?编程方面吗?如果是的话,我不知道。

在日常生活中?例如,说说“我”和我的“手表”,它们是如何同步或异步工作的?

“我”什么时候与我的“手表”同步?

用秒表。

很好。你开始理解了。 😄。

那么,我可以说裁判(包括主裁判)根据一个时间同步赛跑者,而这个时间是在独立运行且不影响赛跑者的吗?

有没有办法让我自己弄明白?

很高兴看到你主动学习更多知识。😁 既然如此,我只为你分解一些更小的问题,供你探索发现。

竞态条件

什么是“运行时”

什么是“同步”

首先,我们需要理解竞态条件,这里有一些很好的问题:

  1. 为什么需要异步地处理事情?
  2. 有哪些类型的异步活动可供我们使用?
  3. 我们日常生活中是否有任何异步发生的事情?(提供一些例子)
  4. 在学校的户外竞赛中,一个领导者如何带领2个或更多的团队成员?
  5. 假设在一场有8名选手的100码短跑比赛中,我们如何坚定地确定谁是最终的获胜者?

这些应该是你探索之旅的一个良好开端。

hollowaykeanho:

是的,但是这两个实体(同时发生)会相互交互吗?

异步是同步的反义词,其中两个或更多实体同时发生但彼此不交互。

有趣。我有点明白了。

hollowaykeanho:

2 entity

是指两行代码或两个代码项吗?

hollowaykeanho:

就像,你不会每秒都手动去转动你的手表齿轮,或者去“滴答”你的手机处理器(就像我们心脏的心跳),对吧?

非常有趣。

那么?我执行一个动作(设置手表),这个动作导致齿轮运转。我执行动作,这个动作相对于手表实际运行来说是异步的。

秒表的例子帮助我理解了。

我们可以看看这个定义吗? 异步 指一种计算机控制时序协议,其中特定操作在收到指示(信号)表明前一个操作已完成时开始。(Google)

我想我明白了。 哦!我多么希望我有更多时间

第一个外来词的意思是同时发生……不是吗?

是的,但是这两个实体(同时发生)会相互交互吗?

异步是同步的反义词,指的是两个或多个实体同时发生但彼此不交互。

那么“我”什么时候与我的“手表”同步呢?

是我戴上它的时候吗?或者可能是我使用它的时候?我有一块智能手表,是我启动健身应用的时候吗?或者对于传统手表,是我设置时间和日期的时候吗?

所以当你戴上它、使用它、启动应用、设置日期时,另一个实体(时间、手表和应用)是否在工作并不重要,对吗?就像你不会手动每秒转动表内的齿轮,或者让你的手机处理器跳动(像我们心脏的心跳)一样,对吗?

因此,我可以说,在你和手表之间,你们本质上是异步工作的,因为你有一颗让你活着的心脏,而手表有自己的电池来驱动齿轮。

附言:我将范围缩小到“我们-手表”以专注于主题。

hollowaykeanho: 在我们的例子中,“我们”作为人类是一个实体,而“手表”是另一个。

明白了。谢谢!

顺便说一句,我很喜欢这个网站的布局。

hollowaykeanho: 所以这两个词都源于“同步”,意思是在给定时间内两个或更多实体之间的交互。

是的。

hollowaykeanho: 这是一个同步行为的例子。手表和你就是两个实体。然而,你是在某个时间点,根据你想要的时间与手表进行同步。

有趣。

hollowaykeanho:

你并不会每秒都手动去转动你手表的齿轮,或者去“滴答”你的手机处理器(就像我们心脏的心跳一样),对吧?

对的。

hollowaykeanho: 你(人)和手表是两个实体,但彼此之间并没有互动。

我明白了。

hollowaykeanho: 那么,在继续深入之前,我们是否已经清楚两个实体之间的同步(同步和异步)了?

是的 🙂

顺便说一句,我一直在研究“runtime”和“sync”,并开始有所理解,但如果能用通俗的语言解释一下,我将不胜感激。

是指两行代码或两个代码项吗?

目前没有代码。实体指的是主体。在我们的例子中,“我们”作为人类是一个实体,“手表”是另一个实体。

同步 基本上意味着你一次只能执行一件事。异步 意味着你可以同时执行多件事,并且不必先完成当前正在执行的事情就可以继续执行下一件。

所以这两个词都源自“同步”,意思是在给定时间内两个或多个实体之间的交互。

那么?我执行一个动作(设置手表)导致齿轮转动。

这是一个同步动作的例子。手表和你是两个实体。然而,你是在与手表就你想要的时间进行同步。

秒表的例子帮助我理解。

你不需要每秒手动转动你的手表齿轮或手动触发你的手机处理器(就像我们心脏的心跳一样),对吧?

这些是异步的。当你不读取/设置手表时,它仍然会滴答作响,以便自行“计数”时间(齿轮通过电池供电旋转)。你和手表是两个实体,但彼此之间没有交互。

类似地,秒表只在“开始”和“停止”时与你同步。在等待“停止”时,它有自己的电池来跟踪时间,因此,状态变为异步。


到目前为止,在继续深入之前,我们是否清楚两个实体之间的同步(同步和异步)?

hollowaykeanho:

异步地?

我需要查一下这个词 😄 尽管根据我遥远的拉丁语学习经历,我大概能猜出它的意思。

计算·电信

指一种计算机控制时序协议,其中特定操作在收到前一个操作已完成的指示(信号)后开始。

好的,我想我明白了。

所以看起来你是想让我回答这些问题。

hollowaykeanho:

为什么事情要异步进行

什么样的事情?编程方面吗?如果是的话,我不知道。

hollowaykeanho:

我们可以使用哪些类型的异步活动?

你是指在 Go 语言中吗?我想我才刚刚入门。

hollowaykeanho:

我们日常生活中有什么事情是异步发生的吗?(提供一些例子)

让我想想。

不同时存在或发生。

你指的是一个过程吗?比如阅读,我们一次读一个词?或者吃饭,我们一次咀嚼一口?

hollowaykeanho:

在学校的户外竞赛中,领导者如何带领 2 名或更多团队成员

嗯。看起来他会在鼓舞士气时同时和整个团队讲话,但在个别指导时会单独沟通。

hollowaykeanho:

假设在有 8 名竞争者的 100 码短跑比赛中,我们如何坚定地确定谁是最终获胜者

用秒表。

cherilexvold1974:

我一直在研究“runtime”和“sync”,并开始理解了

我建议暂时把它放一放。休息一下,尝试先关闭其他“标记为已解决”的话题。你堆积的事情越多,负担和干扰就越大。一次只专注于一件事,最终事情会按时且有效地完成。🙃

cherilexvold1974:

hollowaykeanho:

到目前为止,在继续深入之前,我们是否清楚两个实体(同步和异步)之间的同步?

是的 🙂

现在,假设你戴着一块没有屏幕但会说话的智能手表。你设置它每15分钟报时一次,以便与你同步。因此,同步动作每15分钟发生一次。

出于某种原因,你戴着两块这样的手表,一块说英语,一块说非英语。两块表完全同步,都是每15分钟报时一次。

作为听众,当第一个15分钟到来时,会发生什么?当它们同时用不同的语言报时,你能听清楚吗?


不确定这如何成为一个解决方案,所以我将编辑它以匹配问题:

当两块手表同时试图用不同的语言报时,由于听众一次只能听一个,两块手表都在争夺听众的注意力。这就是竞态条件。

为了解决这个竞态问题,同时保留两块手表(我肯定是疯了),主人必须明确控制两块手表的报时时间。有很多方法可以做到这一点,但值得注意的有两种:

  1. 让手表的报时信息被推送到一个应用程序,就像推送通知一样。然后让通知器来报时。
  2. 设置一块手表在15分/45分报时;另一块在0分/30分报时。

第一种方式是异步消息传递。现在所有4个实体(包括应用程序)都可以自由地相互交互。这种现象在我们身边随处可见:从厨房烹饪食物到送到你的餐桌上。

第二种是同步解决方案,其中的线程被有目的地调整以避免相互冲突。这种现象就像规划你的日历,你不会希望两个事件在同一时间冲突。

为了支持第一种方式,Go提供了 channel 数据类型来进行消息传递。

为了支持第二种方式,Go提供了 sync 包来管理同步。

在Go语言中,竞态条件(Race Condition)是指多个goroutine在没有正确同步的情况下并发访问共享数据,导致程序行为不可预测。Todd McCleod提供的代码示例展示了典型的竞态条件问题。

代码分析

原示例代码(简化版):

package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main() {
	counter := 0
	const gs = 100
	var wg sync.WaitGroup
	wg.Add(gs)

	for i := 0; i < gs; i++ {
		go func() {
			v := counter
			runtime.Gosched()
			v++
			counter = v
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println("counter:", counter)
}

runtime包解析

runtime包提供了与Go运行时系统交互的功能。在竞态条件分析中,关键函数是:

// runtime.Gosched() 让出当前goroutine的执行权
runtime.Gosched()

// 查看逻辑CPU数量
fmt.Println("CPU cores:", runtime.NumCPU())

// 设置最大并发执行的goroutine数量
runtime.GOMAXPROCS(1)

// 查看当前goroutine数量
fmt.Println("Goroutines:", runtime.NumGoroutine())

runtime.Gosched()主动让出CPU时间片,使其他goroutine有机会执行,这有助于暴露竞态条件。

sync包解析

sync包提供了基本的同步原语。解决竞态条件的关键组件:

// 使用互斥锁解决竞态条件
var mu sync.Mutex
mu.Lock()
counter++
mu.Unlock()

// 使用原子操作解决竞态条件
import "sync/atomic"
var counter int64
atomic.AddInt64(&counter, 1)

// WaitGroup等待goroutine完成
var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    // 工作代码
}()
wg.Wait()

竞态条件复现代码

package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main() {
	// 设置使用所有CPU核心
	runtime.GOMAXPROCS(runtime.NumCPU())

	// 竞态条件演示
	fmt.Println("=== Race Condition Demo ===")
	demoRaceCondition()

	// 使用互斥锁解决
	fmt.Println("\n=== Mutex Solution ===")
	demoMutexSolution()

	// 使用原子操作解决
	fmt.Println("\n=== Atomic Solution ===")
	demoAtomicSolution()
}

func demoRaceCondition() {
	counter := 0
	const gs = 100
	var wg sync.WaitGroup
	wg.Add(gs)

	for i := 0; i < gs; i++ {
		go func() {
			v := counter
			runtime.Gosched() // 增加竞态条件发生概率
			v++
			counter = v
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Printf("Expected: 100, Got: %d\n", counter)
}

func demoMutexSolution() {
	counter := 0
	const gs = 100
	var wg sync.WaitGroup
	var mu sync.Mutex
	wg.Add(gs)

	for i := 0; i < gs; i++ {
		go func() {
			mu.Lock()
			v := counter
			runtime.Gosched()
			v++
			counter = v
			mu.Unlock()
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Printf("Expected: 100, Got: %d\n", counter)
}

func demoAtomicSolution() {
	var counter int64
	const gs = 100
	var wg sync.WaitGroup
	wg.Add(gs)

	for i := 0; i < gs; i++ {
		go func() {
			atomic.AddInt64(&counter, 1)
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Printf("Expected: 100, Got: %d\n", counter)
}

检测竞态条件

使用Go内置的竞态检测器:

go run -race main.go
go build -race main.go

竞态检测器会报告类似这样的错误:

WARNING: DATA RACE
Read at 0x00c00001c0a8 by goroutine 7:
  main.demoRaceCondition.func1()
      /path/to/main.go:30 +0x47

Previous write at 0x00c00001c0a8 by goroutine 6:
  main.demoRaceCondition.func1()
      /path/to/main.go:32 +0x63

理解runtime和sync包的方法

  1. 查看官方文档
go doc runtime
go doc sync
go doc sync/atomic
  1. 查看源码
# 查看runtime包源码
cd $(go env GOROOT)/src/runtime

# 查看sync包源码
cd $(go env GOROOT)/src/sync
  1. 编写测试代码
package main

import (
	"fmt"
	"runtime"
	"sync"
	"time"
)

func exploreRuntime() {
	fmt.Println("GOROOT:", runtime.GOROOT())
	fmt.Println("GOOS:", runtime.GOOS)
	fmt.Println("Compiler:", runtime.Compiler)
	
	// 内存统计
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("Alloc = %v MiB\n", m.Alloc/1024/1024)
}

func exploreSync() {
	// Once示例
	var once sync.Once
	onceBody := func() {
		fmt.Println("Only once")
	}
	
	for i := 0; i < 5; i++ {
		go func() {
			once.Do(onceBody)
		}()
	}
	time.Sleep(time.Second)
}

竞态条件的核心问题是并发访问共享状态缺乏同步。runtime包提供运行时控制,sync包提供同步原语。正确使用这些工具可以避免竞态条件,确保并发程序的正确性。

回到顶部