目录
场景有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。
例子:
package main
import (
"fmt"
"sync"
)
var (
x int64
sw sync.WaitGroup
)
func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
sw.Done()
}
func main() {
sw.Add(2)
go add()
go add()
sw.Wait()
fmt.Println(x)
}
运行结果
8740
10000
上面的代码中我们开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。
互斥锁互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:
package main
import (
"fmt"
"sync"
)
var (
x int64
sw sync.WaitGroup
lock sync.Mutex
)
func add() {
for i := 0; i < 5000; i++ {
//加锁
lock.Lock()
x = x + 1
//解锁
lock.Unlock()
}
sw.Done()
}
func main() {
sw.Add(2)
go add()
go add()
sw.Wait()
fmt.Println(x)
}
使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。
读写互斥锁
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
示例:
互斥锁
package main
import (
"fmt"
"sync"
"time"
)
var (
x int64
//计数器
sw sync.WaitGroup
//互斥锁
lock sync.Mutex
//读写锁
rwlock sync.RWMutex
)
//读写函数
//读函数
func read() {
defer sw.Done()
//读锁加锁
//rwlock.RLock()
//互斥锁
lock.Lock()
time.Sleep(time.Microsecond * 5)
//rwlock.RUnlock() //读锁解锁
//解锁
lock.Unlock()
}
//写函数
func write() {
defer sw.Done()
//写锁加锁
//rwlock.Lock()
lock.Lock()
//写入5毫秒
x++
time.Sleep(time.Microsecond * 5)
lock.Unlock()
//rwlock.Unlock() //写锁解锁
}
func main() {
//启动时间戳
start := time.Now()
//写入10次
for i := 0; i < 10; i++ {
sw.Add(1)
go write()
}
sw.Wait()
end1 := time.Now()
fmt.Printf("写入使用时间%v\n", end1.Sub(start))
//读1000次
for i := 0; i < 1000; i++ {
sw.Add(1)
go read()
}
sw.Wait()
end := time.Now()
fmt.Printf("读使用时间%v\n", end.Sub(start))
}
运行结果
PS D:\golang\src\dev_ops\day30\lock\demo3> go run .\main.go
写入使用时间149.4821ms
读使用时间15.8658353s
PS D:\golang\src\dev_ops\day30\lock\demo3> go run .\main.go
写入使用时间117.2153ms
读使用时间15.7770074s
读写锁
package main
import (
"fmt"
"sync"
"time"
)
var (
x int64
//计数器
sw sync.WaitGroup
//互斥锁
lock sync.Mutex
//读写锁
rwlock sync.RWMutex
)
//读写函数
//读函数
func read() {
defer sw.Done()
//读锁加锁
rwlock.RLock()
//互斥锁
//lock.Lock()
time.Sleep(time.Microsecond * 5)
rwlock.RUnlock() //读锁解锁
//解锁
//lock.Unlock()
}
//写函数
func write() {
defer sw.Done()
//写锁加锁
rwlock.Lock()
//lock.Lock()
//写入5毫秒
x++
time.Sleep(time.Microsecond * 5)
//lock.Unlock()
rwlock.Unlock() //写锁解锁
}
func main() {
//启动时间戳
start := time.Now()
//写入10次
for i := 0; i < 10; i++ {
sw.Add(1)
go write()
}
sw.Wait()
end1 := time.Now()
fmt.Printf("写入使用时间%v\n", end1.Sub(start))
//读1000次
for i := 0; i < 1000; i++ {
sw.Add(1)
go read()
}
sw.Wait()
end := time.Now()
fmt.Printf("读使用时间%v\n", end.Sub(start))
}
运行结果
PS D:\golang\src\dev_ops\day30\lock\demo3> go run .\main.go
写入使用时间137.349ms
读使用时间152.6864ms
PS D:\golang\src\dev_ops\day30\lock\demo3> go run .\main.go
写入使用时间150.1936ms
读使用时间165.9248ms
PS D:\golang\src\dev_ops\day30\lock\demo3> go run .\main.go
写入使用时间101.9337ms
读使用时间117.9023ms
Sync.Once
延迟一个开销很大的初始化操作,到真正用到它的时候再执行,例如:定义了一个init初始化函数,程序启动的时候会被自动加载,无论是否用到都会加载,这样程序就会增加程序的启动延时。
在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。
sync.Once只有一个Do方法,其签名如下:
func (o *Once) Do(f func()) {}
注意:如果要执行的函数f需要传递参数就需要搭配闭包来使用。
加载配置文件示例
延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。我们来看一个例子:
var icons map[string]image.Image
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 被多个goroutine调用时不是并发安全的
func Icon(name string) image.Image {
if icons == nil {
loadIcons()
}
return icons[name]
}
多个goroutine并发调用Icon函数时不是并发安全的,现代的编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons函数可能会被重排为以下结果:
func loadIcons() {
icons = make(map[string]image.Image)
icons["left"] = loadIcon("left.png")
icons["up"] = loadIcon("up.png")
icons["right"] = loadIcon("right.png")
icons["down"] = loadIcon("down.png")
}
在这种情况下就会出现即使判断了icons不是nil也不意味着变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但是这样做又会引发性能问题。
使用sync.Once改造的示例代码如下:
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。
示例
package main
import (
"fmt"
"sync"
)
var only sync.Once
func test(x int) {
fmt.Println(x)
}
//闭包
func close(x int) func() {
return func() {
test(x)
}
}
func main() {
//函数变量
t := close(10)
only.Do(t)
}
运行结果
10
sync.Map
Go语言中内置的map不是并发安全的。请看下面的示例:
package main
import (
"fmt"
"strconv"
"sync"
)
var m = make(map[string]int)
//设置map
func set(key string, value int) {
m[key] = value
}
//获取map值
func get(key string) int {
return m[key]
}
func main() {
sw := sync.WaitGroup{}
for i := 0; i < 20; i++ {
sw.Add(1)
go func (n int) {
key := strconv.Itoa(n) //整形转字符串
set(key,n) //设置map元素
fmt.Printf("key: %s,value:%v\n",key,get(key)) //输出map元素
sw.Done()
}(i)
}
sw.Wait()
}
运行结果
PS D:\golang\src\dev_ops\day30\syncMap\test> go run .\main.go
key: 0,value:0
fatal error: key: 19,value:19
key: 5,value:5
key: 6,value:6
key: 7,value:7
key: 8,value:8
key: 9,value:9
key: 10,value:10
key: 11,value:11
key: 12,value:12
key: 13,value:13
key: 14,value:14
key: 4,value:4
key: 15,value:15
concurrent map writeskey: 16,value:16
key: 17,value:17
key: 18,value:18
goroutine 7 [running]:
runtime.throw({0x549dcc, 0x0})
C:/Program Files/Go/src/runtime/panic.go:1198 +0x76 fp=0xc000055ed0 sp=0xc000055ea0 pc=0x4d3116
runtime.mapassign_faststr(0x0, 0x0, {0x54d5cd, 0x1})
C:/Program Files/Go/src/runtime/map_faststr.go:211 +0x39c fp=0xc000055f38 sp=0xc000055ed0 pc=0x4b067c
main.set(...)
D:/golang/src/dev_ops/day30/syncMap/test/main.go:13
main.main.func1(0x1)
D:/golang/src/dev_ops/day30/syncMap/test/main.go:26 +0x6c fp=0xc000055fc8 sp=0xc000055f38 pc=0x52deac
main.main·dwrap·1()
D:/golang/src/dev_ops/day30/syncMap/test/main.go:29 +0x2d fp=0xc000055fe0 sp=0xc000055fc8 pc=0x52de0d
runtime.goexit()
C:/Program Files/Go/src/runtime/asm_amd64.s:1581 +0x1 fp=0xc000055fe8 sp=0xc000055fe0 pc=0x4fca81
created by main.main
D:/golang/src/dev_ops/day30/syncMap/test/main.go:24 +0x38
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x0)
C:/Program Files/Go/src/runtime/sema.go:56 +0x25
sync.(*WaitGroup).Wait(0x0)
C:/Program Files/Go/src/sync/waitgroup.go:130 +0x71
main.main()
D:/golang/src/dev_ops/day30/syncMap/test/main.go:32 +0xea
goroutine 8 [runnable]:
internal/poll.runtime_Semacquire(0xe)
C:/Program Files/Go/src/runtime/sema.go:61 +0x25
internal/poll.(*fdMutex).rwlock(0xc00008e280, 0x54)
C:/Program Files/Go/src/internal/poll/fd_mutex.go:154 +0xd2
internal/poll.(*FD).writeLock(...)
C:/Program Files/Go/src/internal/poll/fd_mutex.go:239
internal/poll.(*FD).Write(0xc00008e280, {0xc000014220, 0x10, 0x10})
C:/Program Files/Go/src/internal/poll/fd_windows.go:598 +0x6c
os.(*File).write(...)
C:/Program Files/Go/src/os/file_posix.go:49
os.(*File).Write(0xc000006018, {0xc000014220, 0x12, 0xc000057f98})
C:/Program Files/Go/src/os/file.go:176 +0x65
fmt.Fprintf({0x567fa0, 0xc000006018}, {0x548f59, 0x12}, {0xc000057f98, 0x2, 0x2})
C:/Program Files/Go/src/fmt/print.go:205 +0x9b
fmt.Printf(...)
C:/Program Files/Go/src/fmt/print.go:213
main.main.func1(0x2)
D:/golang/src/dev_ops/day30/syncMap/test/main.go:27 +0x11b
created by main.main
D:/golang/src/dev_ops/day30/syncMap/test/main.go:24 +0x38
goroutine 9 [semacquire]:
internal/poll.runtime_Semacquire(0xe)
C:/Program Files/Go/src/runtime/sema.go:61 +0x25
internal/poll.(*fdMutex).rwlock(0xc00008e280, 0x54)
C:/Program Files/Go/src/internal/poll/fd_mutex.go:154 +0xd2
internal/poll.(*FD).writeLock(...)
C:/Program Files/Go/src/internal/poll/fd_mutex.go:239
internal/poll.(*FD).Write(0xc00008e280, {0xc000014230, 0x10, 0x10})
C:/Program Files/Go/src/internal/poll/fd_windows.go:598 +0x6c
os.(*File).write(...)
C:/Program Files/Go/src/os/file_posix.go:49
os.(*File).Write(0xc000006018, {0xc000014230, 0x12, 0xc00009bf98})
C:/Program Files/Go/src/os/file.go:176 +0x65
fmt.Fprintf({0x567fa0, 0xc000006018}, {0x548f59, 0x12}, {0xc00009bf98, 0x2, 0x2})
C:/Program Files/Go/src/fmt/print.go:205 +0x9b
fmt.Printf(...)
C:/Program Files/Go/src/fmt/print.go:213
main.main.func1(0x3)
D:/golang/src/dev_ops/day30/syncMap/test/main.go:27 +0x11b
created by main.main
D:/golang/src/dev_ops/day30/syncMap/test/main.go:24 +0x38
exit status 2
上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。
像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
package main
import (
"fmt"
"strconv"
"sync"
)
var m = sync.Map{}
func main() {
sw := sync.WaitGroup{}
for i := 0; i < 20; i++ {
sw.Add(1)
go func(n int) {
key := strconv.Itoa(n) //整形转字符串
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("key: %s,value:%v\n", key, value)
sw.Done()
}(i)
}
sw.Wait()
}
运行结果
PS D:\golang\src\dev_ops\day30\syncMap\Map> go run .\syncmap.go
key: 0,value:0
key: 4,value:4
key: 1,value:1
key: 2,value:2
key: 3,value:3
key: 11,value:11
key: 5,value:5
key: 6,value:6
key: 7,value:7
key: 8,value:8
key: 9,value:9
key: 10,value:10
key: 15,value:15
key: 12,value:12
key: 13,value:13
key: 14,value:14
key: 17,value:17
key: 16,value:16
key: 18,value:18
key: 19,value:19
定时器
Ticker:时间到了,多次执行
package main
import (
"fmt"
"time"
)
func main() {
// 1.获取ticker对象
ticker := time.NewTicker(1 * time.Second)
i := 0
// 子协程
go func() {
for {
//<-ticker.C
i++
fmt.Println(<-ticker.C)
if i == 5 {
//停止
ticker.Stop()
}
}
}()
for {
}
}
运行结果
PS D:\golang\src\dev_ops\day30\time\demo1> go run .\main.go
2022-04-27 21:36:31.8552045 +0800 CST m=+1.005777801
2022-04-27 21:36:32.8665091 +0800 CST m=+2.017082401
2022-04-27 21:36:33.8661172 +0800 CST m=+3.016690501
2022-04-27 21:36:34.8629154 +0800 CST m=+4.013488701
2022-04-27 21:36:35.8553368 +0800 CST m=+5.005910101
exit status 0xc000013a
示例二
package main
import (
"fmt"
"time"
)
func tickDemo() {
//定义计时器
ticker := time.Tick(time.Second*2) //两秒执行一次
for i := range ticker {
fmt.Println(i)
}
}
func main() {
tickDemo()
}
运行结果
PS D:\golang\src\dev_ops\day30\time\demo2> go run .\ticker.go
2022-04-27 21:39:15.2020295 +0800 CST m=+2.010088101
2022-04-27 21:39:17.2070939 +0800 CST m=+4.015152501
2022-04-27 21:39:19.2033854 +0800 CST m=+6.011444001
2022-04-27 21:39:21.1949244 +0800 CST m=+8.002983001
2022-04-27 21:39:23.2007962 +0800 CST m=+10.008854801
2022-04-27 21:39:25.2087759 +0800 CST m=+12.016834501
2022-04-27 21:39:27.2055817 +0800 CST m=+14.013640301
exit status 0xc000013a