Go语言竞争状态讲解

    两个或者多个goroutine 有并发,就有资源竞争,如果两个或者多个 goroutine 在没有相互同步的情况下,访问某个共享的资源,比如同时对该资源进行读写时,就会处于相互竞争的状态,这就是并发中的资源竞争。竞争状态的存在是让并发程序变得复杂的地方,十分容易引起潜在的问题。对共享资源的操作必须原子化的,同一时刻只能有一个goroutine对共享资源进行读和写操作。

//演示程序中存在的竞争状态

package main

import (

        "fmt"

        "runtime"

        "sync"

)

var (

        //counter 是所有goroutine都要增加其值的变量

        counter int

        wg      sync.WaitGroup

)

//main函数是所有go程序的入口

func main() {

        //计数加2,表示等待两个goroutine

        wg.Add(2)

        //创建两个goroutine

        go inCounter(1)

        go inCounter(2)

        //等待gorountine结束

        wg.Wait()

        fmt.Println("Final counter \n", counter)

}

func inCounter(id int) {

        //在函数退出时调用Done来通知main函数工作已经完成

        defer wg.Done()

        for count := 0; count < 2; count++ {

                //捕获 counter的值

                value := counter

                //当前goroutine 从线程退出,并回到队列

                runtime.Gosched()

                //增加本地value变量值

                value++

                //将该值保存回counter

                counter = value

        }

}

 

下面的代码

package main

import (

    "fmt"

    "runtime"

    "sync"

)

var (

    count int32

    wg    sync.WaitGroup

)

func main() {

    wg.Add(2)

    go incCount()

    go incCount()

    wg.Wait()

    fmt.Println(count)

}

func incCount() {

    defer wg.Done()

    for i := 0; i < 2; i++ {

        value := count

        runtime.Gosched()

        value++

        count = value

    }

}

这个是一个竞争的例子,程序多运行几次,结果可能是2,也可能是3,还可能是4。这个是因为count变量没有任何同步保护,所以两个goroutine都是会对其进行读写,会到知识已经计算好的结果被覆盖,以至于产生错误的结果。代码中的 runtime.Gosched() 是让当前 goroutine 暂停的意思,退回执行队列,让其他等待的 goroutine 运行,目的是为了使资源竞争的结果更明显。

下面我们来分析一下程序的运行过程,将两个 goroutine 分别假设为 g1 和 g2:

  • g1 读取到 count 的值为 0;
  • 然后 g1 暂停了,切换到 g2 运行,g2 读取到 count 的值也为 0;
  • g2 暂停,切换到 g1,g1 对 count+1,count 的值变为 1;
  • g1 暂停,切换到 g2,g2 刚刚已经获取到值 0,对其 +1,最后赋值给 count,其结果还是 1;
  • 可以看出 g1 对 count+1 的结果被 g2 给覆盖了,两个 goroutine 都 +1 而结果还是 1。

通过上面的分析可以看出,之所以出现上面的问题,是因为两个 goroutine 相互覆盖结果。

通过运行结果可以看出 goroutine 8 在代码 25 行读取共享资源value := count,而这时 goroutine 7 在代码 28 行修改共享资源count = value,而这两个 goroutine 都是从 main 函数的 16、17 行通过 go 关键字启动的。

 

一种修正代码、消除竞争状态的办法是:使用go语言提供的锁机制,来锁住共享资源,从而保证goroutine的同步状态。
锁住共享资源

Go语言提供了传统的同步 goroutine 的机制,就是对共享资源加锁。atomic 和 sync 包里的一些函数就可以对共享的资源进行加锁操作。

原子函数

原子函数能够以很底层的加锁机制来同步访问整型变量和指针,示例代码如下所示:

package main

import (

        "fmt"

        "runtime"

        "sync"

        "sync/atomic"

)

var (

        counter int64

        wg      sync.WaitGroup

)

func main() {

        wg.Add(2)

        go incCounter(1)

        go incCounter(2)

        wg.Wait() //等待goroutine结束

        fmt.Println("Final counter:", counter)

}

func incCounter(id int) {

        defer wg.Done()

        for count := 0; count < 2; count++ {

                atomic.AddInt64(&counter, 1) //安全的对counter加1

                runtime.Gosched()

        }

}

    上述代码中使用了 atmoic 包的 AddInt64 函数,这个函数会同步整型值的加法,方法是强制同一时刻只能有一个 gorountie 运行并完成这个加法操作。当 goroutine 试图去调用任何原子函数时,这些 goroutine 都会自动根据所引用的变量做同步处理。另外两个有用的原子函数是 LoadInt64 和 StoreInt64。这两个函数提供了一种安全地读和写一个整型值的方式。下面是代码就使用了 LoadInt64 和 StoreInt64 函数来创建一个同步标志,这个标志可以向程序里多个 goroutine 通知某个特殊状态。

package main

import (

        "fmt"

        "sync"

        "sync/atomic"

        "time"

)

var (

        shutdown int64

        wg       sync.WaitGroup

)

func main() {

        wg.Add(2)

        go doWork("A")

        go doWork("B")

        time.Sleep(1 * time.Second)

        fmt.Println("Shutdown Now")

        atomic.StoreInt64(&shutdown, 1)

        wg.Wait()

}

func doWork(name string) {

        defer wg.Done()

        for {

                fmt.Printf("Doing %s Work\n", name)\

                time.Sleep(250 * time.Millisecond)

                if atomic.LoadInt64(&shutdown) == 1 {

                        fmt.Printf("Shutting %s Down\n", name)

                        break

                }

        }

}

上面代码中 main 函数使用 StoreInt64 函数来安全地修改 shutdown 变量的值。如果哪个 doWork goroutine 试图在 main 函数调用 StoreInt64 的同时调用 LoadInt64 函数,那么原子函数会将这些调用互相同步,保证这些操作都是安全的,不会进入竞争状态。

互斥锁

另一种同步访问共享资源的方式是使用互斥锁,互斥锁这个名字来自互斥的概念。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界代码。

package main

import (

        "fmt"

        "runtime"

        "sync"

)

var (

        counter int64

        wg      sync.WaitGroup

        mutex   sync.Mutex

)

func main() {

        wg.Add(2)

        go incCounter(1)

        go incCounter(2)

        wg.Wait()

        fmt.Println("Final counter=", counter)

}

func incCounter(id int) {

        defer wg.Done()

        for count := 0; count < 2; count++ {

                //同一时刻只允许一个goroutine进入这个临界区

                mutex.Lock()

                {

                        value := counter

                        runtime.Gosched()

                        value++

                        counter = value

                }

                mutex.Unlock() //释放锁,允许其他正在等待的goroutine进入临界区

        }

}

输出:

    Final counter= 4

    同一时刻只有一个 goroutine 可以进入临界区。之后直到调用 Unlock 函数之后,其他 goroutine 才能进去临界区。当调用 runtime.Gosched 函数强制将当前 goroutine 退出当前线程后,调度器会再次分配这个 goroutine 继续运行。

发布了37 篇原创文章 · 获赞 19 · 访问量 859

猜你喜欢

转载自blog.csdn.net/zuiyijiangnan/article/details/104127987