Golang竞争条件与锁

竞争条件指的是程序在多个线程交叉执行时,引发的不可预知的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

var data int

func main() {
go func() {
data++
}()

if data==0{
fmt.Printf("the value is %v.\n", data)
}

}

这段代码可能会出现如下三种不同情况:

  • 不打印任何东西;第6行早于第9行执行
  • 打印the value is 0;第9行和第10行早于第6行执行
  • 打印the value is 1;执行第9行后,然后执行第6行,再执行第10行

竞态会导致程序的结果是不可预期的,而且不方便发现和调试。
怎么解决这个问题呢?
最简单的方法就是对访问到了变量的地方进行加锁操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"
import "sync"
var data int
var mu sync.Mutex
func main() {

go func() {
mu.Lock()
data++
mu.Unlock()
}()
mu.Lock()

if data==0{
fmt.Printf("the value is %v.\n", data)
}
mu.Unlock()
}

image
这样的话就不会产生竞态问题,但这样看上去不怎么优雅,而且锁用多了会很影响程序的性能。

也可以使用golangchannel,使data++操作早于print,所以下面的程序不会输出任何东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

var data int

var d = make(chan int)
func main() {

go func() {
data++
d <- data

}()
data = <-d //如果没有数据将一直等待
if data == 0{
fmt.Printf("the value is %v.\n", data)
}

}


数据竞争发生的条件:

  • 两个或两个以上的线程同时访问相同的变量
  • 最少一个为写操作
  • 没有使用任何同步机制

一.未使用任何同步机制的例子

1
2
3
4
5
6
7
8
9
10
11
12
// vim bank.go
package bank

var balance int

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

func Balance() int {
return balance
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// vim bank_main.go
package main

import (
"fmt"
"github.com/bank"
)

func main() {
// Alice
go func() {
bank.Deposit(200)
fmt.Println("=", bank.Balance())
}()
// Bob
go bank.Deposit(100)
}

执行go run src/bank_main.go,意外的发现没有打印任何信息。这是为什么呢?因为运行Go程序时,至少会启动一个main goroutine,当main goroutine早于其它协程执行之前退出,其它协程还没来得及执行就结束了。
执行go run -race src/bank_main.go却打印了= 200
image

当两个goroutine同时访问balance变量时,假如Alice读取了balance并要将数据写入到balance的时候,如果此时切换到Bob,并将数据写入到了balance,然后再切换到Alice,并写入balance,这样就会将Bob的操作进行覆盖,这就是发生了数据竞争。
二.使用channel避免多个goroutine访问变量,解决数据竞争问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package bank

var deposits = make(chan int) // send amount to deposit
var balances = make(chan int) // receive balance

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

func teller() {
var balance int // balance is confined to teller goroutine
for {
select {
case amount := <-deposits:
balance += amount
case balances <- balance:
}
}
}

func init() {
go teller() // start the monitor goroutine
}

执行go run -race src/bank_main.go未发现竟争条件问题
image

三.允许多个goroutine访问变量,但是在同一个时刻最多只有一个goroutine访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
)

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

func Deposit(amount int) {
sema <- struct{}{}
balance += amount
fmt.Print("balance", balance)
<-sema

}

func Balance() int {
sema <- struct{}{}
b := balance
<-sema
return b
}

func main() {
go Deposit(10)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "sync"
var (
mu sync.Mutex // guards balance
balance int
)
func Deposit(amount int) {
mu.Lock()
balance = balance + amount
mu.Unlock()
}
func Balance() int {
mu.Lock()
b := balance
mu.Unlock()
return b
}

使用Lockchannel有什么优缺点?


Deadlocks, Livelocks, Starvation

Deadlocks(死锁),在维基百科的解释为:In concurrent computing, a deadlock is a state in which each member of a group is waiting for another member, including itself, to take action, such as sending a message or more commonly releasing a lock.
image
图片来源:wikipedia

进程P1持有资源R2,进程P2持有资源R1,当进程P1请求资源R1,进程P2请求资源R2的时候这种情况下就会发生死锁,如果没有外部干预进程会一直卡死。

死锁发生必须的4个条件,因为这个4个条件来源的于G. Coffman, Jr.的一篇,因此也被称为Coffman conditions

  • 互斥:一个资源在同一时间只能被一个进程访问
  • 等待条件:进程同时占用至少一个资源,而且同时请求对其它的资源
  • 非抢占:资源只能被占用它的那个进程释放
  • 循环等待:比如P1等待P2释放资源,P2等待P1释放,形成了环状结构

使用Golang模拟死锁发生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
"sync"
"time"
)
type value struct {
mu sync.Mutex
value int
}


var wg sync.WaitGroup

var printSum = func(v1,v2 *value) {

defer wg.Done()

v1.mu.Lock()

defer v1.mu.Unlock()

time.Sleep(2 * time.Second)

v2.mu.Lock()

defer v2.mu.Unlock()
fmt.Printf("sum=%v\n",v1.value+v2.value)
}

func main() {
var a, b value
wg.Add(2)
go printSum(&a, &b) //Goroutine p1
go printSum(&b, &a) // Goroutine p2
wg.Wait()
}

image
Golang居然检测出来发生了死锁,那Golang是如何进行死锁检测的呢?

image
图片来源:Concurrenecy in Go

程序运行时,goroutine p1获取a锁,此时p1持有a的锁,休眠,然后goroutine p2开始执行,并持有资源b的锁,然后p1请求资源b,p2请求资源a,发生死锁。

Livelocks(活锁)
活锁就是程序一直在运行,但是一直没有改变进程本身的状态。举个例子,比如你走在一个狭窄的通道,对面迎过来一个人,他看见了你就走到了另一边打算让你通过,而你也走到了另一边,当你们一直重复这两个动作时就发生了活锁。

Starvation
In computer science, resource starvation is a problem encountered in concurrent computing where a process is perpetually denied necessary resources to process its work.
在计算机科学中,资源匮乏是并发计算中遇到的一个问题,在该问题中,永久拒绝某个进程来处理其工作所需的资源


Ref:
1.https://en.wikipedia.org/wiki/Race_condition
2.The Go programing language
3.The Go Memory Model
4.Concurrency in Go
5.https://en.wikipedia.org/wiki/Deadlock
6.(https://github.com/kat-co/concurrency-in-go-src(https://github.com/kat-co/concurrency-in-go-src)
7.https://en.wikipedia.org/wiki/Starvation_(computer_science))