第九章 基于共享变量的并发(四)内存同步

2018-01-25  本文已影响0人  HaoR_W

一、内存同步

潜在问题

问题:以下代码段的所有可能输出结果是什么

var x, y int
go func() {
    x = 1 // A1
    fmt.Print("y:", y, " ") // A2
}()
go func() {
    y = 1                   // B1
    fmt.Print("x:", x, " ") // B2
}()
一般会想到的:
y:0 x:1
x:0 y:1
x:1 y:1
y:1 x:1

**注意** 以下的也会出现:
x:0 y:0
y:0 x:0

内存中数据的变化不一定是实时的

假设时间上B2在A1之后执行,B2读到的x的值不一定是A1修改后的,因为可能还没有同步

原因

并发 != 不同goroutines中的语句交错执行

在现代计算机中可能会有一堆处理器,每一个都会有其本地缓存(local cache)。为了效率,对内存的写入一般会在每一个处理器中缓冲,并在必要时一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的顺序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器将其聚集的写入flush并commit,这样goroutine在某个时间点上的执行结果才能被其它处理器上运行的goroutine得到[1]

解决思路

在一个独立的goroutine中,语句的效果(effect)是被确保按顺序发生的,也就是说goroutine是顺序连贯(sequentially consistent)的。但如果没有使用mutex或者channel来进行显式的同步,就无法保证这些事件在其他的goroutine看来也是按照同样的顺序(核心还是主存与缓存的同步问题)。

所有这些并发的问题都可以用简单、既定的模式来规避:尽量将变量限定在goroutine内部;如果是多个goroutines都需要访问的变量,则使用互斥条件来访问(无论是读还是写)。

二、Happens Before

为了更好地描述并发程序中事件的顺序关系,Go的文档中提到了“happens before”的概念[2]

定义

假设A和B表示一个多线程的程序执行的两个操作。如果A happens before B,那么A操作对内存的影响将在B被执行之前对执行B的线程可见。

一些规则

文档中也提到了判断 happens before的一些规则:

happens-before不是时序关系[3]

关注的是对内存中数据的影响

三、sync.Once

为了实现变量的懒初始化(lazy initialization),且使之可被并发访问。

var icons map[string]image.Image

func loadIcons() {
    icons = map[string]image.Image{
        "spades.png":   loadIcon("spades.png"),
        "hearts.png":   loadIcon("hearts.png"),
        "diamonds.png": loadIcon("diamonds.png"),
        "clubs.png":    loadIcon("clubs.png"),
    }
}

// 不是并发安全的!!
func Icon(name string) image.Image {
    if icons == nil {
        loadIcons() // one-time initialization
    }
    return icons[name]
}

除了竞争问题之外,还有一个问题。由于编译器和CPU可以重排语句顺序,loadIcons()可能实际变成:

func loadIcons() {
    // 在这句之后在其他goroutines看来就可能不为nil了,但其实初始化并没有完成
    icons = make(map[string]image.Image)
    
    icons["spades.png"] = loadIcon("spades.png")
    icons["hearts.png"] = loadIcon("hearts.png")
    icons["diamonds.png"] = loadIcon("diamonds.png")
    icons["clubs.png"] = loadIcon("clubs.png")
}

修改1:使用互斥锁sync.Mutex

var mu sync.Mutex // guards icons
var icons map[string]image.Image

// 并发安全的,因为Mutex会触发内存同步
func Icon(name string) image.Image {
    mu.Lock()
    defer mu.Unlock()
    if icons == nil {
        loadIcons()
    }
    return icons[name]
}

问题:不能并发访问

修改2:使用读写锁sync.RWMutex

var mu sync.RWMutex // guards icons
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
    mu.RLock()
    if icons != nil {
        icon := icons[name]
        mu.RUnlock()
        return icon
    }
    mu.RUnlock()

    // acquire an exclusive lock
    mu.Lock()
    // NOTE: must recheck for nil 因为之前释放过锁,故可能已被其他goroutine初始化过了
    if icons == nil { 
        loadIcons()
    }
    icon := icons[name]
    mu.Unlock()
    return icon
}

问题:太复杂,容易写错

推荐方案:使用sync.Once(原因在“happens before”的讨论中提到过)

var loadIconsOnce sync.Once
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}

每一次对Do(loadIcons)的调用都会锁定mutex,并会检查boolean变量。在第一次调用时,变量的值是falseDo会调用loadIcons并会将boolean设置为true。随后的调用什么都不会做,但是mutex同步会保证loadIcons对内存(这里其实就是指icons变量啦)产生的效果能够对所有goroutine可见。用这种方式来使用sync.Once的话,我们能够避免在变量被构建完成之前和其它goroutine共享该变量[4]





1/24/2018


  1. Go语言圣经 - 9.4

  2. The Go Memory Model

  3. 深入解析Go - 10.1

  4. Go语言圣经 - 9.5

上一篇下一篇

猜你喜欢

热点阅读