共享内存并发机制

659 阅读3分钟

问题引入

我们先看下下面的代码,看看输出的结果是什么,符合我们的预期吗?

func main() {
    counter := 0
    for i := 0; i < 5000; i++ {
        go func() {
            counter++
        }()
    }
    time.Sleep(time.Second * 1)
    fmt.Println("counter = ", counter)
}

运行过上面代码的同学应该都发现了:

  • 每次的输出结果都是不一样的
  • 每次的结果都不是5000

造成上述问题的原因就是该程序不是线程安全的,每个协程拿到的counter的值是不一样的,可能有些协程快些,counter的值就大,有些则不然

解决

这里我们可以尝试着用锁来处理

func TestCounterThreadSafe(t *testing.T) {
    var mut sync.Mutex

    counter := 0
    for i := 0; i < 5000; i++ {
        go func() {
            defer func() {
                mut.Unlock()
            }()
            mut.Lock()
            counter++
        }()
    }
    time.Sleep(time.Second * 1)
    fmt.Println("counter = ", counter)
}

image.png 这里我们引入锁的概念,大家可以理解成,现在每个协程都在抢着要counter变量,就相当于每个协程要去上厕所,目前只有一个厕所,每个协程谁先抢到了厕所,就在厕所上上锁,那么别的协程想要上厕所就只能在外面等着,等抢到counter的协程处理完后再用counter做处理

分析一

我们上面的代码可能有些同学就发现了,为什么还有加上一个time.Sleep()呢?好,那我们去掉这个等待看看结果会怎么样?

func TestCounterThreadSafe(t *testing.T) {
    var mut sync.Mutex
    counter := 0
    for i := 0; i < 5000; i++ {
        go func() {
            defer func() {
                mut.Unlock()
            }()
            mut.Lock()
            counter++
        }()
    }
    fmt.Println("counter = ", counter)
}

image.png 大家可以看到,结果又和我们的预期出现了错乱。 原因呢就是,main函数是一个主协程,其他由go修饰的函数是子协程,当主协程结束退出后,子协程也会跟着死亡,无论子协程的代码是否执行完。所以我们在前面加上1秒的等待时间是给子携程争取时间的

分析二

因为我们不知道这5000个协程执行完成后到底会花多长时间,所以我们自己决定就等1秒,但这样多少是不严谨的。 假如我们有一个这样的场景,当所有的5000个协程执行总共要花1分钟,但是我们的主协程就等1秒中,这种情况会导致协程都还没执行完程序就中断了 为了处理这种情况,换句话说,我们不想手动计算所有协程的总耗时,go语言给我们提供了这样一个方法:只有等所有被注册的协程执行完后,才能执行后续的操作

func TestCounterThreadSafe(t *testing.T) {
    var mut sync.Mutex
    var wg sync.WaitGroup
    counter := 0
    for i := 0; i < 5000; i++ {
        wg.Add(1) // 注册一个协程
        go func() {
            defer func() {
                mut.Unlock()
            }()
            mut.Lock()
            counter++
            wg.Done() // 协程完成
        }()
    }
    wg.Wait() // 等待所有注册的协程完成后在执行后续操作
    fmt.Println("counter = ", counter)
}

写在最后

好,到现在为止,协程安全的最终实现就是这样,锁处理数据安全,wg处理防止主协程退出 大家一定要注意,锁和wg解决的问题是不一样的