go语言之并发资源竞争

author author     2022-09-03     404

关键词:

并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。


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都会对其进行读写,会导致对已经计算好的结果覆盖,以至于产生错误结果。这里我们演示一种可能,两个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提供了一个工具来帮助我们检查,这个就是go build -race命令。我们在当前项目目录下执行这个命令,生成一个可以执行文件,然后再运行这个可执行文件,就可以看到打印出的检测信息。


go build -race


多加了一个-race标志,这样生成的可执行程序就自带了检测资源竞争的功能。下面我们运行,也是在终端运行。


./hello


我这里示例生成的可执行文件名是hello,所以是这么运行的。这时候,我们看终端输出的检测结果。


  hello ./hello       
==================
WARNING: DATA RACE
Read at 0x0000011a5118 by goroutine 7:
  main.incCount()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:25 +0x76

Previous write at 0x0000011a5118 by goroutine 6:
  main.incCount()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:28 +0x9a

Goroutine 7 (running) created at:
  main.main()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:17 +0x77

Goroutine 6 (finished) created at:
  main.main()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:16 +0x5f
==================
4
Found 1 data race(s)


看,找到一个资源竞争,连在那一行代码出了问题,都标示出来了。goroutine 7在代码 25 行读取共享资源value := count,而这时goroutine 6正在代码 28 行修改共享资源count = value,而这两个goroutine都是从main函数启动的,在 16、17 行,通过go关键字。


既然我们已经知道共享资源竞争的问题,是因为同时有两个或者多个goroutine对其进行了读写,那么我们只要保证,同时只有一个goroutine读写不就可以了。现在我们就看下传统解决资源竞争的办法——对资源加锁


Go语言提供了atomic包和sync包里的一些函数对共享资源同步枷锁,我们先看下atomic包。


package main

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

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 := atomic.LoadInt32(&count)
        runtime.Gosched()
        value++
        atomic.StoreInt32(&count,value)
    }
}


留意这里atomic.LoadInt32atomic.StoreInt32两个函数:一个读取int32类型变量的值,一个是修改int32类型变量的值。这两个都是原子性的操作,Go已经帮助我们在底层使用加锁机制,保证了共享资源的同步和安全,所以我们可以得到正确的结果。这时候我们再使用资源竞争检测工具go build -race检查,也不会提示有问题了。


atomic包里还有很多原子化的函数可以保证并发下资源同步访问修改的问题。比如函数atomic.AddInt32可以直接对一个int32类型的变量进行修改,在原值的基础上再增加多少的功能,也是原子性的。这里不再举例,大家自己可以试试。


atomic虽然可以解决资源竞争问题,但是比较都是比较简单的,支持的数据类型也有限,所以Go语言还提供了一个sync包。这个sync包里提供了一种互斥型的锁,可以让我们自己灵活地控制那些代码,同时只能有一个goroutine访问,被sync互斥锁控制的这段代码范围,被称之为临界区。临界区的代码,同一时间,只能又一个goroutine访问。刚刚那个例子,我们还可以这么改造。


package main

import (
    "fmt"
    "runtime"
    "sync"
)

var (
    count int32
    wg    sync.WaitGroup
    mutex sync.Mutex
)

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++ {
        mutex.Lock()
        value := count
        runtime.Gosched()
        value++
        count = value
        mutex.Unlock()
    }
}


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


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


这种方式比较灵活,可以让代码编写者任意定义需要保护的代码范围,也就是临界区。除了原子函数和互斥锁,Go还为我们提供了更容易在多个goroutine同步的功能,这就是通道chan,我们会在下次继续讲解。


本文出自 “baby神” 博客,请务必保留此出处http://babyshen.blog.51cto.com/8405584/1933211

go语言之通道

上一篇我们讲的原子函数和互斥锁,都可以保证共享数据的读写。但是呢,它们还是有点复杂,而且影响性能。对此,Go又为我们提供了一种工具,这就是通道。所以在多个goroutine并发中,我们不仅可以通过原子函数和互斥锁保... 查看详情

《go语言实战》摘录:6.3并发-竞争状态

6.3并发-竞争状态 查看详情

go语言之并发示例-pool

针对这个资源池管理的一步步都实现了,而且做了详细的讲解,下面就看下整个示例代码,方便理解。package commonimport (    "errors"    "io"    "sync"    "log")//一 查看详情

go语音之进阶篇多任务资源竞争问题

 1、多任务资源竞争问题示例:packagemainimport( "fmt" "time")//定义一个打印机,参数为字符串,按每个字符打印//打印机属于公共资源funcPrinter(strstring) for_,data:=rangestr fmt.Printf("%c",data) time.Sleep(time.Second) fmt.Printf("")funcperson1() Print... 查看详情

go语言学习之旅--并发编程

Go语言学习之旅--并发编程golang并发编程之协程golang并发编程之通道golang并发编程之WaitGroup实现同步golang并发编程之runtime包golang并发编程之Mutex互斥锁实现同步golang并发编程之channel的遍历golang并发编程之selectswitchgolang并发编程之T... 查看详情

go语言学习之旅--并发编程

Go语言学习之旅--并发编程golang并发编程之协程golang并发编程之通道golang并发编程之WaitGroup实现同步golang并发编程之runtime包golang并发编程之Mutex互斥锁实现同步golang并发编程之channel的遍历golang并发编程之selectswitchgolang并发编程之T... 查看详情

go语言之go语言锁机制(代码片段)

Go语言锁机制Go语言互斥锁Go语言的sync包中实现了两种锁Mutex(互斥锁)和RWMutex(读写锁),其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能。互斥锁Mutex是互斥锁,有Lock()加锁、Unlock()解锁两个方法,使用Lock()加... 查看详情

go语言,用原子函数atomic避免资源竞争

下一步应该是互斥锁了。packagemainimport( "fmt" "runtime" "sync" "sync/atomic")var( counterint64 wgsync.WaitGroup)funcmain(){ wg.Add(2) fmt.Println("CreateGoroutines") goincCounter(1) goincCounter(2) fmt.Printl 查看详情

go语言中的数据竞争模式

...更多的Go开发人员,去关注并发代码的编写,考虑不同的语言的特性、以及避免由于自身编程习惯所引发的并发错误。近年来,Uber已经开始采用Golang(简称Go)作为开发微服务的主要编程语言。目前,其Gomonorepo(译者注:包含多... 查看详情

go语言系列之并发编程(代码片段)

Go语言中的并发编程并发与并行并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。Go语言的并发通过goroutine实现。goroutine类似于线程,... 查看详情

go语言学习之旅--并发编程

Go语言学习之旅--并发编程golang并发编程之协程golang并发编程之通道golang并发编程之WaitGroup实现同步golang并发编程之runtime包golang并发编程之Mutex互斥锁实现同步golang并发编程之channel的遍历golang并发编程之selectswitchgolang并发编程之T... 查看详情

go_11:go语言基础之并发concurrency

...称的高并发的根本原因。另外,goroutine的简单易用,也在语言层面上给予了开发者巨大的遍历。  高并发当中一定要注意:并发可不是并行。   查看详情

go语言基础之并发concurrency

...称的高并发的根本原因。另外,goroutine的简单易用,也在语言层面上给予了开发者巨大的遍历。  高并发当中一定要注意:并发可不是并行。   查看详情

go语言channel(代码片段)

多线程同步问题互斥锁互斥锁的本质是当一个goroutine访问的时候,其它goroutine都不能访问这样就能实现资源同步,但是在避免资源竞争的同时也降低了程序的并发性能.程序由原来的并发执行变成了串行案例:有一个打印函数,用于逐... 查看详情

《go语言实战》摘录:6.4并发-锁住共享资源

6.4锁住共享资源  查看详情

go语言基础之并发(代码片段)

并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。Go语言中的并发编程并发与并行并发:同一时间段内执行多个任务并行:同一时刻执行多个任务Go语言的并发通过goro... 查看详情

go语言基础之并发(代码片段)

Go语言中的并发编程——并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。并发与并行并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。并... 查看详情

go语言基础之并发和网络

1、goroutine在这章中将展示Go使用channel和goroutine开发并行程序的能力。goroutine是Go并发能力的核心要素。但是,goroutine到底是什么?叫做goroutine是因为已有的短语——线程、协程、进程等等——传递了不准确的含义。goroutine有简单... 查看详情