golang并发资源的竞争

go是通过协程goroutine来实现并发的,goroutine创建短小轻量级的开销让人确实着迷,它屏蔽了OS层面多线程的操作,如果写过Java代码我们知道并发程序的设计和要考虑到的问题是多么让人头疼了,当然并不是说Java不好,而且Java现在通过某种手段也能实现类似goroutine的功能,语言本身不重要,重要的是它实用的场景,关于Java和Go的长处短处这里也不展开了,但是可以知道一点只要有并发必然会涉及到资源的竞争,那么goroutine是如何避免这个问题的呢?当然阅读本篇文章假设我们已经有了go的语言基础,可以看易佰教程迅速入门https://www.yiibai.com/go/

我们先简单的看一个并发资源争夺的例子:

package main
import (
    "fmt"
    "runtime"
    "sync"
)
var (
    count int32
    wg    sync.WaitGroup //WaitGroup总共有三个方法:Add(delta int),Done(),Wait(),简单的说一下这三个方法的作用:Add:添加或者减少等待goroutine的数量,Done:相当于Add(-1),Wait:执行阻塞,直到所有的WaitGrou数量变成0
)

func sCount() {
    defer wg.Done()
    for i := 0; i < 2; i++ {
        value := count
        runtime.Gosched()//把cpu让出来,让另一个goroutine跑
        value++
        count = value
    }
}

func main() {
    wg.Add(2)
    go sCount()
    go sCount()
    wg.Wait()
    fmt.Println(count)
}

我们可以多运行几次这个程序,会发现结果可能是2,也可以是3,也可能是4。因为共享资源count变量没有任何同步保护,所以两个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相互覆盖结果。我们这里的runtime.Gosched()是让当前goroutine暂停的意思,退回执行队列,让其他等待的goroutine运行,目的是让我们演示资源竞争的结果更明显。注意,这里还会牵涉到CPU问题,多核会并行,那么资源竞争的效果更明显。

所以我们对于同一个资源的读写必须是原子化的,也就是说,同一时间只能有一个goroutine对共享资源进行读写操作。

那么怎么解决这个问题呢?go语言提供了atomic包和sync包里的一些函数对共享资源同步枷锁,这两种的包的使用原理都差不多,但atomic支持的数据类型有限,使用sync包里提供了一种互斥型的锁可以更灵活的控制互斥区:

package main
import (
    "fmt"
    "runtime"
    "sync"
)
var (
    count int32
    wg    sync.WaitGroup
    mutex sync.Mutex
)

func sCount() {
    defer wg.Done()
    for i := 0; i < 2; i++ {
        mutex.Lock()
        value := count
        runtime.Gosched()
        value++
        count = value
        mutex.Unlock()
    }
}
func main() {
    wg.Add(2)
    go sCount()
    go sCount()
    wg.Wait()
    fmt.Println(count)
}

实例中,新声明了一个互斥锁mutex sync.Mutex,这个互斥锁有两个方法,一个是mutex.Lock(),一个是mutex.Unlock(),这两个之间的区域就是临界区,临界区的代码是安全的。

示例中我们先调用mutex.Lock()对有竞争资源的代码加锁,这样当一个goroutine进入这个区域的时候,其他goroutine就进不来了,只能等待,一直到调用mutex.Unlock() 释放这个锁为止。

这种方式比较灵活,可以让代码编写者任意定义需要保护的代码范围,也就是临界区。除了原子函数和互斥锁,Go还为我们提供了更容易在多个goroutine同步的功能,这就是通道chan,在通道里我们才能感受到go语言的并发之美,感受到go语言为什么奉行“通过通信来共享内存”的理念!在下一章我们将学习通道chan,感受go的并发之美!!!

猜你喜欢

转载自blog.csdn.net/suresand/article/details/79624231