李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
GO
正文
25.Golang并发安全和锁
Leefs
2022-07-15 PM
1546℃
0条
[TOC] ### 前言 ![25.Golang并发安全和锁01.png](https://lilinchao.com/usr/uploads/2022/07/3135476892.png) ### 一、概述 有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。这就好比现实生活中十字路口被各个方向的汽车竞争,还有火车上的卫生间被车厢里的人竞争。 **数据竞争的示例** ```go import ( "fmt" "sync" ) var ( x int64 sw sync.WaitGroup // 等待组 ) // add 对全局变量x执行5000次加1操作 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) } ``` **运行结果** ``` 6865 ``` 预期结果应该是`10000`,但是为什运行结果是`6865`呢? 在上面的示例代码片中,开启了两个 goroutine 分别执行 add 函数,这两个 goroutine 在访问和修改全局的`x`变量时就会存在数据竞争,某个 goroutine 中对全局变量`x`的修改可能会覆盖掉另一个 goroutine 中的操作,所以导致最后的结果与预期不符。 ### 二、互斥锁 互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个 goroutine 可以访问共享资源。Go 语言中使用`sync`包中提供的`Mutex`类型来实现互斥锁。 **`sync.Mutex`提供了两个方法:** | 方法名 | 功能 | | :----------------------: | :--------: | | func (m *Mutex) Lock() | 获取互斥锁 | | func (m *Mutex) Unlock() | 释放互斥锁 | 在下面的示例代码中使用互斥锁限制每次只有一个 goroutine 才能修改全局变量`x`,从而修复上面代码中的问题。 ```go import ( "fmt" "sync" ) var ( x int64 sw sync.WaitGroup // 等待组 lock sync.Mutex // 互斥锁 ) // add 对全局变量x执行5000次加1操作 func add() { for i := 0; i < 5000; i++ { //加锁 lock.Lock() // 修改x前加锁 x = x + 1 //解锁 lock.Unlock() // 改完解锁 } sw.Done() } func main() { sw.Add(2) go add() go add() sw.Wait() fmt.Println(x) } ``` **运行结果** ``` 10000 ``` 将上面的代码编译后多次执行,每一次都会得到预期中的结果——10000。 使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等待锁;当互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区,多个 goroutine 同时等待一个锁时,唤醒的策略是随机的。 虽然使用互斥锁能解决资源争夺问题,但是并不完美,通过全局变量加锁同步来实现通讯, 并不利于多个协程对全局变量的读写操作。 ### 三、读写互斥锁 互斥锁是完全互斥的,但是实际上有很多场景是读多写少的,当我们并发的去读取一个资源而不涉及资源修改的时候是没有必要加互斥锁的,这种场景下使用读写锁是更好的一种选择。 读写锁在 Go 语言中使用`sync`包中的`RWMutex`类型。 **`sync.RWMutex`提供了以下5个方法:** | 方法名 | 功能 | | :---------------------------------: | :----------------------------: | | func (rw *RWMutex) Lock() | 获取写锁 | | func (rw *RWMutex) Unlock() | 释放写锁 | | func (rw *RWMutex) RLock() | 获取读锁 | | func (rw *RWMutex) RUnlock() | 释放读锁 | | func (rw *RWMutex) RLocker() Locker | 返回一个实现Locker接口的读写锁 | 读写锁分为两种:读锁和写锁。当一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;而当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。 **示例** ```go 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)) } ``` **运行结果** + 互斥锁运行结果 ``` 写入使用时间144.1418ms 读使用时间14.9631592s ``` + 读写锁运行结果 ``` 写入使用时间114.681ms 读使用时间130.2691ms ``` 从最终的执行结果可以看出,使用读写互斥锁在读多写少的场景下能够极大地提高程序的性能。不过需要注意的是如果一个程序中的读操作和写操作数量级差别不大,那么读写互斥锁的优势就发挥不出来。 ### 四、sync.Once 在某些场景下我们需要确保某些操作即使在高并发的场景下也只会被执行一次,例如只加载一次配置文件等。 Go语言中的`sync`包中提供了一个针对只执行一次场景的解决方案——`sync.Once`,`sync.Once`只有一个`Do`方法,其签名如下: ```go func (o *Once) Do(f func()) ``` **注意:**如果要执行的函数`f`需要传递参数就需要搭配闭包来使用。 **示例** ```go 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.Once`其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。 ### 五、sync.Map Go 语言中内置的 map 不是并发安全的,请看下面这段示例代码。 ```go 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() } ``` **运行报错** ``` fatal error: concurrent map writes key: 3,value:3 goroutine 7 [running]: runtime.throw(0xe220b8, 0x15) C:/Users/25567/go/go1.16.15/src/runtime/panic.go:1117 +0x79 fp=0xc000059ec8 sp=0xc000059e98 pc=0xd85fd9 ``` 我们不能在多个 goroutine 中并发对内置的 map 进行读写操作,否则会存在数据竞争问题。 像这种场景下就需要为 map 加锁来保证并发的安全性了,Go语言的`sync`包中提供了一个开箱即用的并发安全版 map——`sync.Map`。开箱即用表示其不用像内置的 map 一样使用 make 函数初始化就能直接使用。同时`sync.Map`内置了诸如`Store`、`Load`、`LoadOrStore`、`Delete`、`Range`等操作方法。 | 方法名 | 功能 | | :----------------------------------------------------------: | :-----------------------------: | | func (m *Map) Store(key, value interface{}) | 存储key-value数据 | | func (m *Map) Load(key interface{}) (value interface{}, ok bool) | 查询key对应的value | | func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) | 查询或存储key对应的value | | func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) | 查询并删除key | | func (m *Map) Delete(key interface{}) | 删除key | | func (m *Map) Range(f func(key, value interface{}) bool) | 对map中的每个key-value依次调用f | **并发读写`sync.Map`示例** ```go import ( "fmt" "strconv" "sync" ) // 并发安全的map var m = sync.Map{} func main() { sw := sync.WaitGroup{} // 对m执行20个并发的读写操作 for i := 0; i < 20; i++ { sw.Add(1) go func(n int) { key := strconv.Itoa(n) //整形转字符串 m.Store(key, n) // 存储key-value value, _ := m.Load(key) // 根据key取值 fmt.Printf("key: %s,value:%v\n", key, value) sw.Done() }(i) } sw.Wait() } ``` **运行结果** ``` key: 19,value:19 key: 1,value:1 key: 0,value:0 key: 6,value:6 key: 3,value:3 key: 2,value:2 key: 4,value:4 key: 18,value:18 key: 5,value:5 key: 17,value:17 key: 7,value:7 key: 9,value:9 key: 14,value:14 key: 13,value:13 key: 15,value:15 key: 10,value:10 key: 11,value:11 key: 16,value:16 key: 12,value:12 key: 8,value:8 ``` *附参考文章链接* *https://www.liwenzhou.com/posts/Go/concurrence/#autoid-0-6-1*
标签:
Golang基础
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2254.html
上一篇
24.Golang之select
下一篇
26.Golang之反射介绍
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
4
标签云
Python
RSA加解密
Scala
递归
HDFS
GET和POST
Spark SQL
正则表达式
Map
gorm
锁
DataX
稀疏数组
链表
Spark Core
JavaScript
Git
JavaWeb
Nacos
Spring
国产数据库改造
DataWarehouse
排序
Jenkins
线程池
数学
CentOS
JavaWEB项目搭建
随笔
栈
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭