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运行,目的是为了使资源竞争的结果更明显
执行过程:
假设两个goroutine为g1和g2:
-
g1读到count值为0 -
g1暂停,g2开始读取count值也是0 -
g2暂停,g1开始继续往下执行,g1对count加1 -
g1执行完毕,g2执行,由于之前获取到的count值为0往下执行的时候不会继续读取一遍count所以继续对count加1赋值给count。所以count的值还是1
综上所述:
出现上诉问题的原因是两个goroutine相互覆盖的结果
解决方案:
对同一个资源的读写必须原子化,同一时间只能允许有一个 goroutine对共享资源进行读写操作
Go提供的解决共享资源的工具
go通过生成一个可执行文件,可打印共享资源的检测信息:
命令:
go build -race
传统的操作共享资源的办法
锁住共享资源
atomic和sync包里的一些函数就可以对共享的资源进行加锁操作
原子函数
特点:
以很底层的加锁机制来同步访问整型变量和指针
示例代码:
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包下的其他原子函数:
LoadInt64和StoreInt64。这两个函数提供了一种安全地读和写一个整型值的方式。
示例代码:
使用LoadInt64和StoreInt64函数来创建一个同步标志,这个标志可以向程序里多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)
}

浙公网安备 33010602011771号