go避免数据竞争

Go用来避免数据竞争的三种方式:

  1. 不要尝试去写变量
  2. 避免从多个goroutine访问变量.(只有一个goroutine访问变量,其他使用channel来发送给指定goroutine来查询或者更新变量。)
    a. 一个提供对一个指定的变量通过channel来请求的goroutine叫做这个变量的监控(monitor)goroutine
    b. 不要通过使用共享数据来通信,而要使用通信来共享数据
  3. 另外一种方法是允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问,这种方式被称为互斥。

示例数据竞争的情况,并通过三种方式进行改造:
数据竞争:
bank.go

package bank

var balance int

func Deposit(amount int) {balance = balance + amount}

func Balance() int { return balance }

main.go

package main

import (
    "demo/shareParams/bank"
    "fmt"
    "sync"
)

func main() {
    fmt.Println("begin")
    wg := sync.WaitGroup{}
    wg.Add(2)
    go func() {
       defer wg.Done()
       bank.Deposit(200)
       fmt.Println("=", bank.Balance())
   }()

    go func() {
       defer wg.Done()
       bank.Deposit(100)
   }()

    wg.Wait()
}

输出结果图片:
数据竞争的运行效果
race检查结果:

F:\github\demo\shareParams>go run -race main.go
begin
= 200
==================
WARNING: DATA RACE
Read at 0x000000637600 by goroutine 8:
  demo/shareParams/bank.Deposit()
      F:/github/demo/shareParams/bank/bank.go:5 +0x6b
  main.main.func2()
      F:/github/demo/shareParams/main.go:21 +0x5f

Previous write at 0x000000637600 by goroutine 7:
  demo/shareParams/bank.Deposit()
      F:/github/demo/shareParams/bank/bank.go:5 +0xac
  main.main.func1()
      F:/github/demo/shareParams/main.go:15 +0x88

Goroutine 8 (running) created at:
  main.main()
      F:/github/demo/shareParams/main.go:19 +0x176

Goroutine 7 (finished) created at:
  main.main()
      F:/github/demo/shareParams/main.go:13 +0x10e
==================
Found 1 data race(s)
exit status 66

第二种方式进行改造

一个提供对一个指定的变量通过channel来请求的goroutine叫做这个变量的监控(monitor)goroutine
不要通过使用共享数据来通信,而要使用通信来共享数据

package main

import (
    "fmt"
    "sync"
)

var deposits = make(chan int)
var balances = make(chan int)

func Deposit(amount int) { deposits <- amount }
func Balance() int { return <-balances }

// 通信来共享数据
// 一个goroutine 来维护变量,
// 其他的通过channel 来通信来查询和更新变量
func teller() {
    var balance int
    for {
        select {
            case amount := <- deposits:
            balance = balance + amount
            case balances <- balance:
            }
    }
}

func init() {
    go teller()
}

func main() {
    fmt.Println("begin")
    wg := sync.WaitGroup{}
    wg.Add(2)
    go func() {
        defer wg.Done()
        Deposit(200)
        fmt.Println("=", Balance())
    }()

    go func() {
        defer wg.Done()
        Deposit(100)
    }()

    wg.Wait()
}

第三种方式进行改造

互斥的方式控制并发
自定义互斥锁 用容量只有1的chan来控制并发访问,保证同一时刻最多只有一个goroutine来访问共享变量。

var (
    sema = make(chan struct{}, 1)
    balance int
)

func Deposite(amount int) {
    sema <- struct{}{} // 获取token
    balance = balance + amount
    <- sema // 释放token
}

func Balance() int {
    sema <- struct{}{} // 获取token
    b := balance
    <- sema // 释放token
    return b
}

用sync.Mutex 互斥锁来实现并发控制:

var (
    mu sync.Mutex
    balance int
)

func Deposit(amount int) {
    mu.Lock()
    balance = balance + amount
    mu.Unlock()
}

func Balance() int {
    mu.Lock()
    b := balance
    mu.Unlock()
    return b
}

互斥锁的不可重入
设计取款函数:

// 这样是错误的
func Withdraw(amount int) bool {
    mu.Lock()
    defer mu.Unlock()
    Deposit(-amount)
    if Balance() < 0{
        Deposit(amount)
        return false
    }
    return true
}

程序报错死锁:
死锁
对一 个已经锁上的mutex来再次上锁–这会导致程序死锁, Deposit 会进行再次上锁,导致的死锁。GoLang 没有重入锁。
有一种小手法去解决这种方式,就是去写个私有的函数deposit (不加锁的) 用在Withdraw上。
示例代码[将Deposit和Balance都替换掉了]:

func Withdraw(amount int) bool {
	mu.Lock()
	defer mu.Unlock()
	deposit(-amount)
	if balance < 0{
		deposit(amount)
		return false
	}
	return true
}

func deposit(amount int) {balance += amount}

读写锁:
用到场景: 多读少写的场景。读也需要占用锁的时间,不能并行的,影响效率的情况。
就能像下面这样去改造之前的函数。

// 允许读并行执行, 但是写操作会完全互斥,多读单写锁
var mu2 sync.RWMutex
func Balance() int {
	mu2.RLock()
	defer mu2.RUnlock()
	return balance
}

有人问到: 为什么不用写锁去改造另外的部分?

我的理解: 我们使用读锁让读的操作可以并发,很快的执行,又不影响写的操作。可是写锁就没有必要了,写是互斥的,原来的sync.Mutex就已经能满足需要了。更何况,sync.RWMutex加了一些复杂的内部记录功能,导致会比原来的sync.Mutex的慢一些。这样就画蛇添足了。

仔细看源码,发现RWMutex底层也是调用sync.Mutex来实现的。
源码为什么Balance这样的函数,只进行了读取,没有其他的操作,还需要进行加锁?

其实底层是内存同步导致的,如果没有限制的并发,可能Balance()
读到的数据是缓存没有同步的数据,GoLang像channel通信或者互斥量操
作这样的原语会使处理器将其聚集的写入内存flush并commit。加入读锁,至少能保证读到的是写入完更新到共享内存中的值。

总结一句话: 将变量限定在 goroutine内部;如果是多个goroutine都需要访问的变量,使用互斥条件来访问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值