001#1. 临界资源:指并发环境中多个进程、线程、协程共享的资源;但是在并发编程中对临界资源的处理不当,往往会导致数据不一致的问题
func main() {
/*
临界资源
*/
a := 1
go func() {
a = 2
fmt.Println("goroutine...", a)
}()
a = 3
fmt.Println("main goroutine", a)
}
因为主的协程执行完之后,关闭了资源,所以子协程没有来的及执行:
main goroutine 3
func main() {
/*
临界资源
*/
a := 1
go func() {
a = 2//14行
fmt.Println("goroutine...", a)
}()
a = 3//18行
time.Sleep(1)
fmt.Println("main goroutine", a)
}
主协程先把赋值为3,在要打印还没打印的时候,睡去之后,子协程持有资源又将其赋值为2
goroutine... 2
main goroutine 2
通过命令行来看一下:
$ go run -race demo10_race.go
==================
//警告:有临界资源
WARNING: DATA RACE
//通过goroutine 6在0x00c00000a0e0处写入:
Write at 0x00c00000a0e0 by goroutine 6:
main.main.func1()
E:/GoPath/src/Go_Advanced/demo10_race.go:14 +0x43
//通过主goroutine在0x00c00000a0e0上一次写入:
Previous write at 0x00c00000a0e0 by main goroutine:
main.main()
E:/GoPath/src/Go_Advanced/demo10_race.go:18 +0x8f
Goroutine 6 (running) created at:
main.main()
E:/GoPath/src/Go_Advanced/demo10_race.go:13 +0x81
==================
main goroutine 2
goroutine... 2
Found 1 data race(s)
exit status 66
2. 并发本身并不复杂,但是因为有了资源竞争的问题,就会有许多莫名其妙的事情
var ticket = 10
func main() {
go sale("售票口1")
go sale("售票口2")
go sale("售票口3")
go sale("售票口4")
time.Sleep(5*time.Second)
}
func sale(name string){
rand.Seed(time.Now().UnixNano())
for {
if ticket > 0 {
time.Sleep(time.Duration(rand.Intn(1000))*time.Microsecond)
fmt.Println(name,"售出:", ticket)
ticket--
} else {
fmt.Println("售完票了")
break
}
}
}
售票口4 售出: 10
售票口2 售出: 10
售票口1 售出: 10
售票口3 售出: 10
售票口4 售出: 6
售票口1 售出: 6
售票口2 售出: 5
售票口3 售出: 5
售票口1 售出: 2
售票口2 售出: 1
售完票了
售票口4 售出: 1
售完票了
售票口3 售出: 0
售完票了
售票口1 售出: -2
售完票了
3. 解决:很多其他语言都是同步方式,通过上锁的方式,及某一段时间段,只能允许一个协程来访问这个共享数据, sync同步包;
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
/**
WaitGroup: 同步等待组
Add(), 设置等待组中要执行的子 goroutine的数量
Wait(), 让主goroutine处于等待
Done(),让等待组中的计数器减一,=在子goroutine中结束等待
*/
wg.Add(2)
go fun1()
go fun2()
fmt.Println("main 进入阻塞状态,等待wg子goroutine结束")
wg.Wait() //表示goroutine进入等待,意味着阻塞
fmt.Println("main..结束阻塞")
}
func fun1() {
for i:= 1; i<10; i++ {
fmt.Println("fun1()函数中打印...A", i)
}
wg.Done()//执行完会后,将计数器减1 wg.Add(-1)
}
func fun2() {
defer wg.Done()
for j:= 1; j<10; j++ {
fmt.Println("\tfun2()函数中打印...A", j)
}
}
4. go并不鼓励用锁来保护临界资源,而是鼓励通过channel将共享状态的变化在各个协程之间传递;要以通信的方式去共享内存;
- 互斥锁(同步):最简单的,也是最暴力的;降低了性能
var ticket = 10
var mutex sync.Mutex
var wg sync.WaitGroup
func main() {
wg.Add(4)
go sale("售票口1")
go sale("售票口2")
go sale("售票口3")
go sale("售票口4")
wg.Wait()//main要等待
fmt.Println("程序结束了")
//time.Sleep(5*time.Second)
}
func sale(name string){
rand.Seed(time.Now().UnixNano())
defer wg.Done()
for {
mutex.Lock()
if ticket > 0 {
time.Sleep(time.Duration(rand.Intn(1000))*time.Microsecond)
fmt.Println(name,"售出:", ticket)
ticket--
} else {
mutex.Unlock()
fmt.Println("售完票了")
break
}
mutex.Unlock()
}
}
- 读写锁:针对读或写的互斥锁,锁可以由任意数量的读取器或单个编写器持有;基本遵循两大原则:可以随便读,多个goroutine同时读;写的时候啥也不能干,不能读也不能写。
var rwMutex *sync.RWMutex
var wg *sync.WaitGroup
func main() {
rwMutex = new(sync.RWMutex)
wg = new(sync.WaitGroup)
wg.Add(2)
go readData(1)
go readData(2)
wg.Wait()
fmt.Println("main over...")
}
func readData(i int){
defer wg.Done()
fmt.Println(i, "开始读: read start...")
rwMutex.RLock()//读操作上锁
fmt.Println(i, "正在读取数据:reading...")
time.Sleep(1*time.Second)
rwMutex.RUnlock()//读操作解锁
fmt.Println(i, "读结束:read over...")
}
2 开始读: read start...
2 正在读取数据:reading...
1 开始读: read start...
1 正在读取数据:reading...
2 读结束:read over...
1 读结束:read over...
main over...
读操作可以同时进行
var rwMutex *sync.RWMutex
var wg *sync.WaitGroup
func main() {
rwMutex = new(sync.RWMutex)
wg = new(sync.WaitGroup)
wg.Add(3)
go writeData(1)
go readData(2)
go writeData(3)
wg.Wait()
fmt.Println("main over...")
}
func writeData(i int) {
defer wg.Done()
fmt.Println(i, "开始写: write start...")
rwMutex.Lock()//读操作上锁
fmt.Println(i, "正在写数据:writing...")
time.Sleep(3*time.Second)
rwMutex.Unlock()//读操作解锁
fmt.Println(i, "写结束:write over...")
}
3 开始写: write start...
3 正在写数据:writing...
2 开始读: read start...
1 开始写: write start...
明显卡顿:必须等3写完,解锁之后才可以;这个时候其他两个协程(1,2)是不能操作的
3 写结束:write over...
2 正在读取数据:reading...
2 读结束:read over...
1 正在写数据:writing...
1 写结束:write over...
main over...