add: golang critical resource

This commit is contained in:
vilet.yy 2021-05-29 16:06:09 +08:00
parent ae9ff52781
commit 5d7dd706ef
1 changed files with 140 additions and 0 deletions

View File

@ -0,0 +1,140 @@
---
layout: post
title: "Golang临界资源安全问题"
date: 2020-05-30 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
- 代码安全
---
### 临界资源
指并发环境中多个进程/线程/协程贡献的资源。
但是在并发编程中对临界资源的处理不当,往往会导致数据不一致的问题。
#### 示例代码
```go
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)
}
```
### 临界资源安全问题
并发本身并不复杂,但是因为有了资源竞争的问题,就使我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。
如果多个goroutine在访问同一个数据资源的时候其中一个线程修改了数据那么这个数值就被修改了对于其他的goroutine来讲这个数值可能是错误的。
举个例子。我们通过并发来实现火车站售票这个功能。一共有100张票4个售票口同时出售
#### 示例代码
```go
package main
import (
"fmt"
"math/rand"
"time"
)
// 全局变量
var ticket = 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 {
if ticket > 0 {
// 睡眠
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
// g1, g3, g2, g4
fmt.Println(name, "售出:", ticket)
ticket--
} else {
fmt.Println(name, "售罄,没有票了。。")
break
}
}
}
```
为了更好的观察临界资源问题每个goroutine先睡眠一个随机数然后再售票发现程序的运行结果还可以卖出编号为负数的票。
#### 分析
我们买票逻辑是先判断票数的编号是否为负数如果大于0然后我们就进行卖票只不过在卖票先睡眠然后再卖假如说此时已经卖票到只剩一张票了某一个goroutine持有了CPU的时间片那么它在判断是否有票的时候条件是成立的所以它可以卖票编号为1的最后一张票。但是因为它在卖之前先睡眠了那么其他的goroutine就会持有CPU的时间片而此时这张票还没有被卖出那么第二个gotoutine再判断是否有票的时候条件也是成立的那么它也可以卖出这张票然后它也进入了睡眠。其他的第三个第四个goroutine都是这样的逻辑当某个goroutine醒来的时候不会再判断是否有票而是直接售出了这样就卖出最后一张票了然而其他的goroutine醒来的时候就会陆续卖出第0张-1张-2张。
这就是临界资源的不安全问题。某一个goroutine在访问某个数据资源的时候按照数值已经判断好了条件然后又被其他的goroutine抢占了资源并修改了数值等这个goroutine再继续访问这个数据的时候数值已经不对了。
### 临界资源安全问题的解决
要想解决临界资源安全的问题很多编程语言的解决方案都是同步。通过上锁的方式某一个时间段只能允许一个goroutine来访问这个共享数据当前goroutine访问完毕解锁后其他的goroutine才能访问。
#### 借助sync包下的锁操作
```go
package main
import (
"fmt"
"math/rand"
"time"
"sync"
)
var ticket = 10
var wg sync.WaitGroup
var matex sync.Mutex // 创建锁头
func main() {
wg.Add(4)
go saleTickets("售票口1")
go saleTickets("售票口2")
go saleTickets("售票口3")
go saleTickets("售票口4")
wg.Wait()
}
func saleTickets(name string) {
rand.Seed(time.Now().UnixNano())
defer wg.Done()
for {
matex.Lock()
if ticket > 0 {
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
fmt.Println(name, "售出:", ticket)
ticket--
} else {
matex.Unlock()
fmt.Println(name, "售罄,没有票了。。")
break
}
}
matex.Unlock()
}
```
### 写在最后
> 不要以共享内存的方式去通信,而要以通信的方式去共享内存。
在Go语言中并不鼓励用锁保护共享状态的方式在不同的Goroutine中分享信息以共享内存的方式去通信。而是鼓励通过**channel**将共享状态或共享状态的变化在各个Goroutine直接传递以通信的方式去共享内存这样同样能像用锁一样保证在同一个时间只有一个Goroutine访问共享状态
当然在主流的编程语言中未来保证多线程直接共享数据安全性和一致性都会提供一套基本的同步工具集如锁条件变量原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制使用方式也和其他语言也差不多。