临界资源安全问题
# 临界资源安全问题
# 一、临界资源
临界资源: 指并发环境汇总多个进程/线程/协程共享的资源.
但是在并发编程中对临界资源的处理不当,往往会导致数据不一致.
示例代码:
package main
import (
"fmt"
"time"
)
func main() {
a := 1
go func() {
a = 2
fmt.Println("子goroutine。。",a)
}()
a = 3
time.Sleep(1)
fmt.Println("main goroutine。。",a)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
低版本go执行
高版本go执行
能够发现一处被多个goroutine
共享的数据.
# 二、临界资源安全问题
并发本身并不复杂,但是因为有力资源竞争的问题,就使得开发出好的并发程序变得复杂起来,因为会引发很多莫名其妙的问题.
如果多个goroutine
在访问一个数据资源的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine
来讲,这个数值可能是不对的.
举个例子,通过并发来实现或者售票这个程序.一共有100张票,4个售票窗口同时出售.
示例代码:
package main
import (
"fmt"
"math/rand"
"time"
)
//全局变量
var ticket = 10 // 100张票
func main() {
/*
4个goroutine,模拟4个售票口,4个子程序操作同一个共享数据。
*/
go saleTickets("售票口1") // g1,100
go saleTickets("售票口2") // g2,100
go saleTickets("售票口3") //g3,100
go saleTickets("售票口4") //g4,100
time.Sleep(5*time.Second)
}
func saleTickets(name string) {
rand.Seed(time.Now().UnixNano())
//for i:=1;i<=100;i++{
// fmt.Println(name,"售出:",i)
//}
for { //ticket=1
if ticket > 0 { //g1,g3,g2,g4
//睡眠
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
// g1 ,g3, g2,g4
fmt.Println(name, "售出:", ticket) // 1 , 0, -1 , -2
ticket-- //0 , -1 ,-2 , -3
} else {
fmt.Println(name,"售罄,没有票了。。")
break
}
}
}
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
39
40
41
42
分析:
买票逻辑是先判断票数的编号是否为负数,如果大于0
,然后就进行买票,只不过在买票前先睡眠,然后再卖,假如说此时已经买票到只剩最后1张了,某一个goroutine
持有了CPU
的时间片,那么它在片段是否有票的时候,条件是成立的,所以它可以买票变为1的最后一张票.但是因为它在卖出之前,先睡眠了,那么其他的goroutine
就会持有CPU
的时间片,而此时这张票还没有卖出,那么第二个goroutine
再判断是否有票的时候,条件也是成立的,那么它可以卖出这张票,然而它也进入了睡眠...其他的第三个第四个goroutine
都是这样的逻辑,当某个goroutine
醒来的时候,不会再判断是否有票,而是直接售出,这样就卖出最后一张票了,然而其他的goroutine
醒来的时候,就会陆续卖出了第0张,-1张,-2张.
这就是临界资源不安全的问题。某一个goroutine
在访问某个数据资源的时候,按照数值,已经判断好了条件,然后又被其他的goroutine
抢占了资源,并修改了数值,等这个goroutine
在继续访问这个数据的时候,数值已经不对了.
# 三、临界资源安全问题的解决
要想解决临界资源的安全问题,很多编程语言的解决方案都是同步.通过上锁的方式,某一时间段,只能允许一个goroutine
来访问这个共享数据,当前goroutine
访问完毕,解锁后,其他的goroutine
才能来访问.
可以借助于sync
包下的锁操作.
示例代码:
package main
import (
"fmt"
"math/rand"
"time"
"sync"
)
//全局变量
var ticket = 10 // 100张票
var wg sync.WaitGroup
var matex sync.Mutex // 创建锁头
func main() {
/*
4个goroutine,模拟4个售票口,4个子程序操作同一个共享数据。
*/
wg.Add(4)
go saleTickets("售票口1") // g1,100
go saleTickets("售票口2") // g2,100
go saleTickets("售票口3") //g3,100
go saleTickets("售票口4") //g4,100
wg.Wait() // main要等待。。。
//time.Sleep(5*time.Second)
}
func saleTickets(name string) {
rand.Seed(time.Now().UnixNano())
defer wg.Done()
//for i:=1;i<=100;i++{
// fmt.Println(name,"售出:",i)
//}
for { //ticket=1
matex.Lock()
if ticket > 0 { //g1,g3,g2,g4
//睡眠
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
// g1 ,g3, g2,g4
fmt.Println(name, "售出:", ticket) // 1 , 0, -1 , -2
ticket-- //0 , -1 ,-2 , -3
} else {
matex.Unlock() //解锁
fmt.Println(name, "售罄,没有票了。。")
break
}
matex.Unlock() //解锁
}
}
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 四、总结
在Go
的并发编程中有一句很经典的话: 不要以共享的方式去通信,而要以通信的方式去共享内存.
在Go
语言中并不鼓励用锁保护共享状态的方式在不同的Goroutine
中分享信息(以共享内存的方式去通信).而是鼓励通过channel
将共享状态或共享状态的变化在各个Goroutine
之间传递(一通信的方式去共享内存),这样同样能像用锁一样保证在同一个Goroutine
访问共享状态.
当然,在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等.Go
语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言差不多.
- 01
- AWS NAT-NetWork-Firwalld配置(一)04-09
- 02
- AWS NAT-NetWork-Firwalld配置(二)04-09
- 03
- kubernetes部署minio对象存储01-18