《快学 Go 语言》第 13 课 —— 并发与安全
上一节我们提到并发编程不同的协程共享数据的方式除了通道之外还有就是共享变量。虽然 Go 语言官方推荐使用通道的方式来共享数据,但是通过变量来共享才是基础,因为通道在底层也是通过共享变量的方式来实现的。通道的内部数据结构包含一个数组,对通道的读写就是对内部数组的读写。
在并发环境下共享读写变量必须要使用锁来控制数据结构的安全,Go 语言内置了 sync 包,里面包含了我们平时需要经常使用的互斥锁对象 sync.Mutex。Go 语言内置的字典不是线程安全的,所以下面我们尝试使用互斥锁对象来保护字典,让它变成线程安全的字典。
线程不安全的字典
Go 语言内置了数据结构「竞态检查」工具来帮我们检查程序中是否存在线程不安全的代码。当我们在运行代码时,打开 -run 开关,程序就会在内置的通用数据结构中进行埋点检查。竞态检查工具在 Go 1.1 版本中引入,该功能帮助 Go 语言「元团队」找出了 Go 语言标准库中几十个存在线程安全隐患的 bug,这是一个非常了不起的功能。同时这也说明了即使是猿界的神仙,写出来的代码也避免不了有 bug。下面我们来尝试一下
package main
import "fmt"
func write(d map[string]int) {
d["fruit"] = 2
}
func read(d map[string]int) {
fmt.Println(d["fruit"])
}
func main() {
d := map[string]int{}
go read(d)
write(d)
}
上面的代码明显存在安全隐患,运行下面的竞态检查指令观察输出结果
$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c420090180 by goroutine 6:
runtime.mapaccess1_faststr()
/usr/local/Cellar/go/1.10.3/libexec/src/runtime/hashmap_fast.go:172 +0x0
main.read()
~/go/src/github.com/pyloque/practice/main.go:10 +0x5d
Previous write at 0x00c420090180 by main goroutine:
runtime.mapassign_faststr()
/usr/local/Cellar/go/1.10.3/libexec/src/runtime/hashmap_fast.go:694 +0x0
main.main()
~/go/src/github.com/pyloque/practice/main.go:6 +0x88
Goroutine 6 (running) created at:
main.main()
~/go/src/github.com/pyloque/practice/main.go:15 +0x59
==================
==================
WARNING: DATA RACE
Read at 0x00c4200927d8 by goroutine 6:
main.read()
~/go/src/github.com/pyloque/practice/main.go:10 +0x70
Previous write at 0x00c4200927d8 by main goroutine:
main.main()
~/go/src/github.com/pyloque/practice/main.go:6 +0x9b
Goroutine 6 (running) created at:
main.main()
~/go/src/github.com/pyloque/practice/main.go:15 +0x59
==================
2
Found 2 data race(s)
竞态检查工具是基于运行时代码检查,而不是通过代码静态分析来完成的。这意味着那些没有机会运行到的代码逻辑中如果存在安全隐患,它是检查不出来的。
线程安全的字典
让字典变的线程安全,就需要对字典的所有读写操作都使用互斥锁保护起来。
package main
import "fmt"
import "sync"
type SafeDict struct {
data map[string]int
mutex *sync.Mutex
}
func NewSafeDict(data map[string]int) *SafeDict {
return &SafeDict{
data: data,
mutex: &sync.Mutex{},
}
}
func (d *SafeDict) Len() int {
d.mutex.Lock()
defer d.mutex.Unlock()
return len(d.data)
}
func (d *SafeDict) Put(key string, value int) (int, bool) {
d.mutex.Lock()
defer d.mutex.Unlock()
old_value, ok := d.data[key]
d.data[key] = value
return old_value, ok
}
func (d *SafeDict) Get(key string) (int, bool) {
d.mutex.Lock()
defer d.mutex.Unlock()
old_value, ok := d.data[key]
return old_value, ok
}
func (d *SafeDict) Delete(key string) (int, bool) {
d.mutex.Lock()
defer d.mutex.Unlock()
old_value, ok := d.data[key]
if ok {
delete(d.data, key)
}
return old_value, ok
}
func write(d *SafeDict) {
d.Put("banana", 5)
}
func read(d *SafeDict) {
fmt.Println(d.Get("banana"))
}
func main() {
d := NewSafeDict(map[string]int{
"apple": 2,
"pear": 3,
})
go read(d)
write(d)
}
尝试使用竞态检查工具运行上面的代码,会发现没有了刚才一连串的警告输出,说明 Get 和 Put 方法已经做到了协程安全,但是还不能说明 Delete() 方法是否安全,因为它根本没有机会得到运行。
在上面的代码中我们再次看到了 defer 语句的应用场景 —— 释放锁。defer 语句总是要推迟到函数尾部运行,所以如果函数逻辑运行时间比较长,这会导致锁持有的时间较长,这时使用 defer 语句来释放锁未必是一个好注意。
避免锁复制
上面的代码中还有一个需要特别注意的地方是 sync.Mutex 是一个结构体对象,这个对象在使用的过程中要避免被复制 —— 浅拷贝。复制将会导致锁被「分裂」了,也就起不到保护的作用。所以在平时的使用中要尽量使用它的指针类型。读者可以尝试将上面的类型换成非指针类型,然后运行一下竞态检查工具,会看到警告信息再次布满整个屏幕。锁复制存在于结构体变量的赋值、函数参数传递、方法参数传递中,都需要注意。
使用匿名锁字段
在结构体章节,我们知道外部结构体可以自动继承匿名内部结构体的所有方法。如果将上面的 SafeDict 结构体进行改造,将锁字段匿名,就可以稍微简化一下代码。
package main
import "fmt"
import "sync"
type SafeDict struct {
data map[string]int
*sync.Mutex
}
func NewSafeDict(data map[string]int) *SafeDict {
return &SafeDict{data, &sync.Mutex{}}
}
func (d *SafeDict) Len() int {
d.Lock()
defer d.Unlock()
return len(d.data)
}
func (d *SafeDict) Put(key string, value int) (int, bool) {
d.Lock()
defer d.Unlock()
old_value, ok := d.data[key]
d.data[key] = value
return old_value, ok
}
func (d *SafeDict) Get(key string) (int, bool) {
d.Lock()
defer d.Unlock()
old_value, ok := d.data[key]
return old_value, ok
}
func (d *SafeDict) Delete(key string) (int, bool) {
d.Lock()
defer d.Unlock()
old_value, ok := d.data[key]
if ok {
delete(d.data, key)
}
return old_value, ok
}
func write(d *SafeDict) {
d.Put("banana", 5)
}
func read(d *SafeDict) {
fmt.Println(d.Get("banana"))
}
func main() {
d := NewSafeDict(map[string]int{
"apple": 2,
"pear": 3,
})
go read(d)
write(d)
}
使用读写锁
日常应用中,大多数并发数据结构都是读多写少的,对于读多写少的场合,可以将互斥锁换成读写锁,可以有效提升性能。sync 包也提供了读写锁对象 RWMutex,不同于互斥锁只有两个常用方法 Lock() 和 Unlock(),读写锁提供了四个常用方法,分别是写加锁 Lock()、写释放锁 Unlock()、读加锁 RLock() 和读释放锁 RUnlock()。写锁是拍他锁,加写锁时会阻塞其它协程再加读锁和写锁,读锁是共享锁,加读锁还可以允许其它协程再加读锁,但是会阻塞加写锁。
读写锁在写并发高的情况下性能退化为普通的互斥锁
下面我们将上面代码中 SafeDict 的互斥锁改造成读写锁。
package main
import "fmt"
import "sync"
type SafeDict struct {
data map[string]int
*sync.RWMutex
}
func NewSafeDict(data map[string]int) *SafeDict {
return &SafeDict{data, &sync.RWMutex{}}
}
func (d *SafeDict) Len() int {
d.RLock()
defer d.RUnlock()
return len(d.data)
}
func (d *SafeDict) Put(key string, value int) (int, bool) {
d.Lock()
defer d.Unlock()
old_value, ok := d.data[key]
d.data[key] = value
return old_value, ok
}
func (d *SafeDict) Get(key string) (int, bool) {
d.RLock()
defer d.RUnlock()
old_value, ok := d.data[key]
return old_value, ok
}
func (d *SafeDict) Delete(key string) (int, bool) {
d.Lock()
defer d.Unlock()
old_value, ok := d.data[key]
if ok {
delete(d.data, key)
}
return old_value, ok
}
func write(d *SafeDict) {
d.Put("banana", 5)
}
func read(d *SafeDict) {
fmt.Println(d.Get("banana"))
}
func main() {
d := NewSafeDict(map[string]int{
"apple": 2,
"pear": 3,
})
go read(d)
write(d)
}
原文发布时间为:2018-12-14
本文作者:老钱
本文来自云栖社区合作伙伴“ 码洞”,了解相关信息可以关注“codehole”微信公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
java B2B2C 源码 多级分销Springboot多租户电子商城系统-springcloud项目redis分布式锁
在springcloud项目开发中redis分布式锁使用主要有两个场景 需要JAVA Spring Cloud大型企业分布式微服务云构建的B2B2C电子商务平台源码请加企鹅求求 :二一四七七七五六三三 1.订单重复提交或支付提交等,防止刷单2.对某个业务进行锁定,例如:当用户同一时间,进行对账户充值和提现操作,那么这里需要根据用户ID对账户进行锁定,只有一个完成了才可以进行第二个。开发实现方式1.pom.xml中引入jar包,最好引入到基础模块中,其他模块通用 <!-- redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 创建redis操作类RedisGlobalLock(自定义)redis提供RedisTemplate方法redis提供三个方法:(1)lock 获取锁并锁定 本方法是立...
-
下一篇
学习Python语言 基础语法:变量的基本使用
Python变量 程序是用来处理数据的,变量就是用来保存数据的,通过给数据定义一个名称来保证方便记忆和识别、使用这个数据。变量可以保存所有类型的数据。 Python变量的定义 在Python中,变量的定义可以不定义变量的类型,这与PHP一样。 同时,在使用变量前必须给变量赋值。(这与上述的观点一致,都没有数据,用啥呢?) 赋值的格式如下: 左边是变量名称,中间使用“=”号,右边为数据,基本可以记忆为“将右边的数据用左边的名称”替代。也可以多变量赋值,如:变量1=变量2=变量3=“数据”,如图: 在学习中有迷茫不知如何学习的朋友小编推荐一个学Python的学习q u n 227 -435- 450可以来了解一起进步一起学习!免费分享视频资料 变量赋值示例 一些Python已经定义的类型 Python有五个标准的数据类型: Numbers(数字) String(字符串) List(列表) Tuple(元组) Dictionary(字典) 其中Numbers支持int、float、long、complex类型 String为字符串,可以使用[头下标:尾下标]来获取子字符串,其中头下标可以从左...
相关文章
文章评论
共有0条评论来说两句吧...