3.2 Go之语言竞争状态

3.2 Go之语言竞争状态

什么是竞争状态?

两个或者多个goroutine在没有相互同步的情况下,访问某个共享的资源,如同时对该资源进行读写时就会处于相互竞争的状态。这就是并发中的资源竞争。

资源竞争示例

package main

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

/* 定义变量 */
var (
   count int32
   wg sync.WaitGroup
)

// 定义资源竞争函数
func intCount() {
   // 异常处理
   wg.Done()
   // 循环对count进行添加
   for i := 0; i < 2; i++ {
       // 将公共资源私有处理
       value := count
       // 设置当前goroutine暂停
       runtime.Gosched()
       // 让私有资源递增
       value++
       // 将递增后的资源赋值给公共资源
       count = value
  }
}

/* 函数调用 */
func main() {
   wg.Add(2)
   // 开启两个goroutine
   go intCount()
   go intCount()
   // 设置等待
   wg.Wait()
   // 打印公共资源
   fmt.Println(count)
}

分析:

多次运行后发现结果不一样。因为count变量没有受到任何同步保护。两个goroutine都会对其进行读写,会导致对已经计算好的结果被覆盖,以至于产生错误结果

  • runtime.Gosched()是让当前goroutine暂停的意思,退回执行队列,让其他等待的goroutine运行,目的是为了使资源竞争的结果更明显

执行过程:

假设两个goroutineg1g2

  • g1读到count值为0

  • g1暂停,g2开始读取count值也是0

  • g2暂停,g1开始继续往下执行,g1count1

  • g1执行完毕,g2执行,由于之前获取到的count值为0往下执行的时候不会继续读取一遍count所以继续对count1赋值给count。所以count的值还是1

综上所述:

出现上诉问题的原因是两个goroutine相互覆盖的结果

解决方案:

对同一个资源的读写必须原子化,同一时间只能允许有一个 goroutine对共享资源进行读写操作

Go提供的解决共享资源的工具

go通过生成一个可执行文件,可打印共享资源的检测信息:

命令:

go build -race

传统的操作共享资源的办法

锁住共享资源

atomicsync包里的一些函数就可以对共享的资源进行加锁操作

原子函数

特点:

以很底层的加锁机制来同步访问整型变量和指针

示例代码:

package main

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

/* 声明变量 */
var (
   counterNo2 int64
   wgNo2 sync.WaitGroup
)

/* 原子函数 */
func intCounter(id int) {
   defer wgNo2.Done()
   // 采用atomic包下的函数进行共享资源自增操作
   for count := 0; count < 2; count++ {
       // 安全的加锁操作共享资源
       atomic.AddInt64(&counterNo2, 1)
       // goroutine返回队列
       runtime.Gosched()
  }
}

func main() {
   wgNo2.Add(2)
   /* 开启两个goroutine */
   go intCounter(1)
   go intCounter(2)
   // goroutine等待
   wgNo2.Wait()
   // 输出结果
   fmt.Println(counterNo2)
}

atmoic包下的AddInt64函数:

同步整型值的加法,方法是强制同一时刻只能有一个gorountie运行并完成这个加法操作。当goroutine试图去调用任何原子函数时,这些goroutine都会自动根据所引用的变量做同步处理。

atmoic包下的其他原子函数:

LoadInt64StoreInt64。这两个函数提供了一种安全地读和写一个整型值的方式。

示例代码:

使用LoadInt64StoreInt64函数来创建一个同步标志,这个标志可以向程序里多goroutine通知某个特殊状态

package main

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

/*
使用StoreInt64函数安全修改公共资源的值
哪个doWork goroutine试图在main函数调用StoreInt64的同时调用LoadInt64函数,那么原子函数会将这些调用互相同步,保证这些操作都是安全的,不会进入竞争状态。
*/
/* 声明变量 */
var (
   shutdown int64
   wgNo3 sync.WaitGroup
)

/*
dowork函数调用loadint64函数让调用互相同步,保证操作的安全
*/
func doWork(name string) {
   // 宕机处理
   defer wgNo3.Done()

   // 循环调用loadint函数
   for {
      fmt.Printf("Doing %s Work\n", name)
      // 线程等待2.5s
      time.Sleep(250 * time.Millisecond)
      /*
      调用atomic包下的loadint函数对公共资源进行判断
       */
       if atomic.LoadInt64(&shutdown) == 1 {
           // 打印结果并跳出循环
           fmt.Printf("Shutting %s Down\n", name)
           break
      }
  }
}

/* 调用 */
func main() {
   // 调用add函数
   wgNo3.Add(2)

   // 调用dowork函数
   go doWork("A")
   go doWork("B")

   // 在main函数中调用StoreInt函数
   atomic.StoreInt64(&shutdown, 1) //在main函数调用StoreInt64的同时调用LoadInt64函数,那么原子函数会将这些调用互相同步,保证这些操作都是安全的,不会进入竞争状态
   wgNo3.Wait()
}

注意:

在关闭goroutine时先打开的后关闭

互斥锁

在代码上给公共资源上锁以后再开始进行公共资源的读写操作。保证同一时间只有一个goroutine在操作公共资源

示例代码:

package main

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

/*
使用互斥锁对公共资源进行锁定
保证同一时间只有一个goroutine在操作公共资源
*/

/* 声明变量 */
var (
   counterNo3 int64
   wgNo4 sync.WaitGroup
   mutex sync.Mutex
)

// 定义互斥函数
func intCounterNo2(id int) {
   // 异常处理--->自动将waitgroup计数器减少1
   defer wgNo4.Done()

   // 循环根据公共资源的值来进行锁定等操作
   for count := 0; count < 2; count++ {
       // 开始加锁
       mutex.Lock()

       // 开始操作公共资源
      {
           value := counterNo3
           // 暂停当前goroutine,退回执行队列.
           runtime.Gosched()
           // 私有变量递增,值传递给公共资源
           value++
           counterNo3 = value
      }

       // 释放锁,允许其他goroutine操作公共资源
       mutex.Unlock()
  }
}

/* 在main函数中对上诉函数进行调用 */
func main() {
   wgNo4.Add(2)

   // 设置两个goroutine访问公共资源
   go intCounterNo2(1)
   go intCounterNo2(2)

   // 设置对应号码的goroutine等待
   wgNo4.Wait()
   // 打印公共资源
   fmt.Println(counterNo3)
}

 

posted @ 2022-03-21 09:21  俊king  阅读(160)  评论(0)    收藏  举报