Golang代码执行结果竟如此惊人

Golang代码执行结果竟如此惊人

package main
import (
	"time"
	"fmt"
)
func in(d *[]int) {
	fmt.Println(*d)
	time.Sleep(time.Duration(2000000000))
	fmt.Println(*d)
}
func main() {
	a := []int{1,2,3}

	go in(&a)
	a = []int{4,5,6}
	//fmt.Printf("")
	for {
	}
}

代码执行结果如下: [1 2 3] [1 2 3]

当移除注释"fmt.Printf("")“的”//"时,代码执行结果变为: [4 5 6] [4 5 6]


更多关于Golang代码执行结果竟如此惊人的实战教程也可以访问 https://www.itying.com/category-94-b0.html

12 回复

这个假设似乎是正确的,但我们应该搜索一些信息来确认这一点。

更多关于Golang代码执行结果竟如此惊人的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


如果我不这样写会发生什么。

任何情况都可能发生。这是未定义的行为。

我知道标准做法应该是使用锁等同步机制来确保程序的正确执行,但我只是想知道如果不这样写会发生什么情况。

WaitGroup、sleep、互斥锁,所有这些都是在通道之上实现的,而通道是一个IO边界,这支持了我的理论。

pamleft:

我只是想知道如果不按照这种方式写会发生什么。

你可能会得到不可预测的结果。就是这样,在并发编程中如果你想要特定的执行顺序,必须自己来实现。

我理解这个"并发"概念。我已经尝试了很多次,无论是否使用printf,结果都是一样的。我猜测某些涉及系统调用的goroutine具有更高的优先级,但我未能找到关于这个话题的相关信息。

根据您的说法,结果应该与现实情况相反。而且打印语句被放置在赋值操作之后,这意味着指令被重新排序了。

我猜想某些涉及系统调用的goroutine具有较高优先级,但我未能找到关于该主题的相关信息。

通常情况下,Go 运行时仅在 I/O 边界处切换协程。运行时可能会意识到主协程中不再发生任何 I/O 操作,因此直接切换到另一个协程,并且永远不会再重新考虑主协程。

不过这只是个假设。

这被称为"并发"。

你永远无法知道哪个 Go 协程会先运行,以及运行时在它们之间如何切换。

在不同的计算机上,打印输出可能会有所不同。

如果你想要严格的边界控制,根据具体需求,你需要使用互斥锁、等待组、信号量,甚至是裸通道。

func main() {
    fmt.Println("hello world")
}
func main() {
    fmt.Println("hello world")
}

当你添加fmt.Printf时,主goroutine在等待标准输出打印时会出现轻微暂停。这使得另一个goroutine能够在进入紧密的for循环之前运行,并获取由&a指向的新数组。

但我同意这个观点。在多个goroutine共享数据时,如果不使用某种同步机制就不应该进行写入操作。即使现在能正常工作,但在运行速度稍快或稍慢的计算机上,或者核心数与你电脑不同的设备上,可能就无法正常工作了。

这真是令人着迷,我不知道为什么会发生这种情况。我最初的想法是变量没有被正确引用,或者存在某种并发问题。但是,这个问题只出现在"for"无限循环中。如果我在底部放置一个睡眠计时器而不是无限循环,问题就会消失。

使用睡眠时间而非无限循环
https://goplay.space/#gMrEqQ0f_Tb

我认为这不太重要,因为无限循环在实践中是有问题的,而且代码本身无法运行。如果能找到某种遇到这种情况的实际用例,那将会很有趣。

如果你想知道如何修复并发问题,那么应该使用WaitGroup,它会在goroutine成功运行之前保持程序,然后再继续执行其余代码。我有一个使用和不使用WaitGroup的示例。

不使用WaitGroup
https://goplay.space/#FRLTD1GqkQ3

使用WaitGroup
https://goplay.space/#OBGVQJT7KhJ

如果你需要防止竞态条件(这是导致此问题从"[1 2 3]“变为”[4 5 6]"的根本原因),那么你需要实现一个互斥锁来在继续之前锁定变量。我也有一个这样的示例。祝你好运。

应用互斥锁
https://goplay.space/#vuYx5ZMPf1R

这是一个典型的并发数据竞争问题,涉及到Go语言的内存模型和编译器优化。让我详细分析一下这个现象:

问题分析

当注释掉 fmt.Printf("") 时,编译器可能会进行优化,导致goroutine看到的是切片 a 的旧值。而加上 fmt.Printf("") 后,这个函数调用阻止了某些优化,使得goroutine能够看到切片 a 的新值。

根本原因

这里存在数据竞争(data race),因为主goroutine和启动的goroutine在没有同步机制的情况下并发访问同一个切片。

package main

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

func in(d *[]int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println(*d)
	time.Sleep(2 * time.Second)
	fmt.Println(*d)
}

func main() {
	var wg sync.WaitGroup
	a := []int{1,2,3}
	
	wg.Add(1)
	go in(&a, &wg)
	
	// 这里存在数据竞争
	a = []int{4,5,6}
	
	wg.Wait()
}

正确解决方案

方案1:使用互斥锁

package main

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

type SafeSlice struct {
	mu sync.RWMutex
	data []int
}

func in(s *SafeSlice, wg *sync.WaitGroup) {
	defer wg.Done()
	
	s.mu.RLock()
	fmt.Println(s.data)
	s.mu.RUnlock()
	
	time.Sleep(2 * time.Second)
	
	s.mu.RLock()
	fmt.Println(s.data)
	s.mu.RUnlock()
}

func main() {
	var wg sync.WaitGroup
	s := &SafeSlice{data: []int{1,2,3}}
	
	wg.Add(1)
	go in(s, &wg)
	
	s.mu.Lock()
	s.data = []int{4,5,6}
	s.mu.Unlock()
	
	wg.Wait()
}

方案2:使用通道进行通信

package main

import (
	"time"
	"fmt"
)

func in(ch <-chan []int, done chan<- bool) {
	data := <-ch
	fmt.Println(data)
	
	time.Sleep(2 * time.Second)
	
	data = <-ch
	fmt.Println(data)
	done <- true
}

func main() {
	ch := make(chan []int, 2)
	done := make(chan bool)
	
	go in(ch, done)
	
	ch <- []int{1,2,3}
	ch <- []int{4,5,6}
	
	<-done
}

方案3:使用原子操作(适用于简单类型)

package main

import (
	"time"
	"fmt"
	"sync/atomic"
)

type AtomicSlice struct {
	ptr atomic.Value
}

func (a *AtomicSlice) Store(slice []int) {
	a.ptr.Store(slice)
}

func (a *AtomicSlice) Load() []int {
	return a.ptr.Load().([]int)
}

func in(a *AtomicSlice, done chan bool) {
	fmt.Println(a.Load())
	time.Sleep(2 * time.Second)
	fmt.Println(a.Load())
	done <- true
}

func main() {
	a := &AtomicSlice{}
	done := make(chan bool)
	
	a.Store([]int{1,2,3})
	go in(a, done)
	
	a.Store([]int{4,5,6})
	<-done
}

关键要点

  1. 避免数据竞争:在并发环境中访问共享数据必须使用同步机制
  2. 不要依赖未定义行为:原代码的执行结果依赖于编译器的具体实现
  3. 使用正确的同步原语:根据场景选择互斥锁、通道或原子操作

原代码中的无限循环 for {} 也应该避免,应该使用 sync.WaitGroup 或通道来正确等待goroutine完成。

回到顶部