超级苦工
阅读 136
《快学 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)
}

下一节我们要开始尝试 Go 语言学习的难点之一 —— 反射。

阅读更多精品文章,微信扫一扫上面的二维码关注公众号「码洞」

关注下面的标签,发现更多相似文章
评论
相关推荐
f'g'h'g'h'f'g'h'g'f'h

发布告白夫妇...

nestjs+vue+ts打造一个酷炫的星空聊天室

简介😛 闲暇时间想做一个聊天室来巩固前端技能,于是在2020年6月24号就开始了阿童木聊天室的开发之旅。😈 项目采用全 typescript 开发,这是为了以后的功能迭代打基础。当然,我本身也是很...

好几个放假

东方红格当然多层次发鬼地方...

《快学 Go 语言》第 16 课 —— 包管理 GOPATH 和 Vendor

来源: 知乎 原文: 《快学 Go 语言》第 16 课 —— 包管理 GOPATH 和 Vendor 到目前位置我们一直在编写单文件代码,只有一个 main.go 文件。本节我们要开始朝完整的项目结构...

dddd

...

《快学 Go 语言》第 15 课 —— 反射

来源: 知乎 原文: 《快学 Go 语言》第 15 课 —— 反射 反射是 Go 语言学习的一个难点,但也是非常重要的一个知识点。反射是洞悉 Go 语言类型系统设计的法宝,Go 语言的 ORM 库离不...

hello

*《快学 Go 语言》第 14 课 —— 魔术变性指针来源: 知乎 原文: 《快学 Go 语言》第 14 课 —— 魔术变性指针 本节我们要学习一些 Go 语言的魔法功能,通过内置的 unsafe 包...

《快学 Go 语言》第 14 课 —— 魔术变性指针

来源: 知乎 原文: 《快学 Go 语言》第 14 课 —— 魔术变性指针 本节我们要学习一些 Go 语言的魔法功能,通过内置的 unsafe 包提供的功能,直接操纵指定内存地址的内存。有了 unsa...

《快学 Go 语言》第 13 课 —— 并发与安全

来源: 知乎 原文: 《快学 Go 语言》第 13 课 —— 并发与安全 上一节我们提到并发编程不同的协程共享数据的方式除了通道之外还有就是共享变量。虽然 Go 语言官方推荐使用通道的方式来共享数据,...

《快学 Go 语言》第 12 课 —— 通道

来源: 知乎 原文: 《快学 Go 语言》第 12 课 —— 通道 不同的并行协程之间交流的方式有两种,一种是通过共享变量,另一种是通过队列。Go 语言鼓励使用队列的形式来交流,它单独为协程之间的队列...

《快学 Go 语言》第 11 课 —— 千军万马跑协程

来源: 知乎 原文: 《快学 Go 语言》第 11 课 —— 千军万马跑协程 协程和通道是 Go 语言作为并发编程语言最为重要的特色之一,初学者可以完全将协程理解为线程,但是用起来比线程更加简单,占用...

测试发布

测试文章发布...

《快学 Go 语言》第 10 课 —— 错误与异常

来源: 知乎 原文: 《快学 Go 语言》第 10 课 —— 错误与异常 Go 语言的异常处理语法绝对是独树一帜,在我见过的诸多高级语言中,Go 语言的错误处理形式就是一朵奇葩。一方面它鼓励你使用 C...

《快学 Go 语言》第 9 课 —— 接口

来源: 知乎 原文: 《快学 Go 语言》第 9 课 —— 接口 接口是一个对象的对外能力的展现,我们使用一个对象时,往往不需要知道一个对象的内部复杂实现,通过它暴露出来的接口,就知道了这个对象具备哪...

《快学 Go 语言》第 8 课 —— 结构体

来源: 知乎 原文: 《快学 Go 语言》第 8 课 —— 结构体 本节我们要开讲 Go 语言在数据结构上最重要的概念 —— 结构体。如果说 Go 语言的基础类型是原子,那么结构体就是分子。分子是原子...

《快学 Go 语言》第 7 课 —— 字符串

来源: 知乎 原文: 《快学 Go 语言》第 7 课 —— 字符串 字符串通常有两种设计,一种是「字符」串,一种是「字节」串。「字符」串中的每个字都是定长的,而「字节」串中每个字是不定长的。Go 语言...

《快学 Go 语言》第 6 课 —— 字典

来源: 知乎 原文: 《快学 Go 语言》第 6 课 —— 字典 字典在数学上的词汇是映射,将一个集合中的所有元素关联到另一个集合中的部分或全部元素,并且只能是一一映射或者多对一映射。<img ...

《快学 Go 语言》第 5 课 —— 灵活的切片

来源: 知乎 原文: 《快学 Go 语言》第 5 课 —— 灵活的切片 切片无疑是 Go 语言中最重要的数据结构,也是最有趣的数据结构,它的英文词汇叫 slice。所有的 Go 语言开发者都津津乐道地...

《快学 Go 语言》第 4 课 —— 低调的数组

来源: 知乎 原文: 《快学 Go 语言》第 4 课 —— 低调的数组 Go 语言里面的数组其实很不常用,这是因为数组是定长的静态的,一旦定义好长度就无法更改,而且不同长度的数组属于不同的类型,之间不...

《快学 Go 语言》第 3 课 —— 分支与循环

来源: 知乎 原文: 《快学 Go 语言》第 3 课 —— 分支与循环 程序 = 数据结构 + 算法上面这个等式每一个初学编程的同学都从老师那里听说过。它并不是什么严格的数据公式,它只是对一般程序的简...