Golang同步示例与错误代码分析

Golang同步示例与错误代码分析

var a string
var done bool

func setup() {
    a = "hello, world"
    done = true
}

func main() {
    go setup()

    for !done {
    }

    print(a)
}

在这个教程中,它声称上面的主函数在某些情况下可能永远不会结束: “更糟糕的是,由于两个线程之间没有同步事件,无法保证主函数能够观察到对done的写入。不能保证main中的循环会结束。”

这怎么可能呢?难道主函数不再读取堆变量done了吗,请解释一下?


更多关于Golang同步示例与错误代码分析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

编译器会优化掉这个循环,因为它没有任何实际作用。之所以这样做,是因为编译器假设 done 变量永远不会被其他协程修改。这种假设是语言规范所允许的,因此编译器可以积极地对编译后的代码进行优化。

这样做的后果是,当我们希望避免此类优化时,就需要显式地使用特殊变量和函数。

这个例子并不能很好地证明同步机制的必要性,因为它没有向不了解编译器假设的读者清晰地展示问题所在。

更多关于Golang同步示例与错误代码分析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这是一个典型的内存可见性问题,涉及到Go内存模型中的happens-before关系。问题确实存在,主函数可能永远不会结束,或者即使结束了也可能打印空字符串。

问题分析

在没有同步机制的情况下,编译器和CPU可能会对指令进行重排序优化。具体来说:

  1. 编译器重排序:编译器可能将setup()函数中的两条赋值语句重新排序
  2. CPU重排序:即使编译器没有重排序,CPU也可能在运行时改变指令执行顺序
  3. 缓存一致性:不同goroutine可能看到不同内存状态

可能的问题场景

// 编译器可能优化后的setup()函数
func setup() {
    done = true  // 先设置done
    a = "hello, world"  // 后设置a
}

在这种情况下,主goroutine可能在done变为true时退出循环,但此时a可能还没有被赋值。

正确的同步方案

方案1:使用sync包

var a string
var done bool
var mu sync.Mutex

func setup() {
    mu.Lock()
    defer mu.Unlock()
    a = "hello, world"
    done = true
}

func main() {
    go setup()

    mu.Lock()
    for !done {
        mu.Unlock()
        time.Sleep(time.Millisecond) // 避免忙等待
        mu.Lock()
    }
    mu.Unlock()
    
    print(a)
}

方案2:使用channel(推荐)

var a string
done := make(chan bool)

func setup() {
    a = "hello, world"
    done <- true
}

func main() {
    go setup()
    <-done  // 等待信号
    print(a)
}

方案3:使用sync.WaitGroup

var a string
var wg sync.WaitGroup

func setup() {
    defer wg.Done()
    a = "hello, world"
}

func main() {
    wg.Add(1)
    go setup()
    wg.Wait()  // 等待goroutine完成
    print(a)
}

方案4:使用atomic(针对简单标志)

var a string
var done int32

func setup() {
    a = "hello, world"
    atomic.StoreInt32(&done, 1)
}

func main() {
    go setup()

    for atomic.LoadInt32(&done) == 0 {
    }
    
    print(a)
}

根本原因

根据Go内存模型,在没有同步机制的情况下,一个goroutine对变量的写入不保证对另一个goroutine立即可见。donea的写入可能被重排序,或者写入结果可能停留在CPU缓存中而没有被刷新到主内存。

这就是为什么教程中警告"无法保证主函数能够观察到对done的写入" - 因为缺少happens-before关系来保证内存操作的可见性顺序。

回到顶部