42

Goroutine 的同步(第三部分)

 5 years ago
source link: https://studygolang.com/articles/16140?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
  • 第一部分:https://studygolang.com/articles/14118
  • 第二部分:https://studygolang.com/articles/14478

mutex 和 sync.Once 介绍

eyuuee2.jpg!web

假设你的程序中有一个需要某种初始化的功能。该 Bootstrap 程序成本很高,因此将其推迟到实际使用功能的那一刻是有意义的。这样,当功能未激活时,就不会浪费 CPU 周期。 这在 Go 中如何完成?

package main

import "fmt"

var capitals map[string]string

func Bootstrap() {
    capitals = make(map[string]string)
    capitals["France"] = "Paris"
    capitals["Germany"] = "Berlin"
    capitals["Japan"] = "Tokyo"
    capitals["Brazil"] = "Brasilia"
    capitals["China"] = "Beijing"
    capitals["USA"] = "Washington"
    ...
    capitals["Poland"] = "Warsaw"
}

func getCapitalCity(country string) string {
    if capitals == nil {
        Bootstrap()
    }
    return capitals[country]
}

func main() {
    fmt.Println(getCapitalCity("Poland"))
    fmt.Println(getCapitalCity("USA"))
    fmt.Println(getCapitalCity("Japan"))
}

你可以想象,如果它可以处理所有的国家,其他类似数据库的结构,使用 I/O 操作等,则 bootstrap 函数可能非常昂贵。上述解决方案看起来简单且优雅,但不幸的是,它并不会正常工作。问题在于当 bootstrap 函数在运行时无法阻止其它 Goroutine 做同样的事。而这些繁重的计算只做一次是很可取的。另外在 capitals 刚刚被初始化而其中的 key 还未设置时,其它 Goroutine 会看到它不为 nil 从而尝试从空的 map 中获取值。

sync.Mutex

YnYrQjr.jpg!web

Go 有包含很多好东西的内置 sync 包。我们可以使用 mutex (mutual exclusion) 来解决我们的同步问题。

import (
    "fmt"
    "sync"
)

...

var (
    capitals map[string]string
    mutex sync.Mutex
)

...

func getCapitalCity(country string) string {
    mutex.Lock()
    if capitals == nil {
        Bootstrap()
    }
    mutex.Unlock()
    return capitals[country]
}

bootstrap 程序可被多次运行的问题解决了。如果任何一个 Goroutine 正在运行 bootstrap 或者甚至在判断 capitals == nil ,那么其它 Goroutine 会在 mutex.Lock() 处等待。 Unlock 函数一结束另一个 Goroutine 就会被“准许进入”。

但是一次只能有一个 Goroutine 执行被放在 mutex.Lock()mutex.Unlock() 之间的代码。因此如果存放首都城市的 map 被多个 Goroutine 读取,那么一切都会在 if 语句处被处理成一个接一个的。从根本上说对于 map 的读操作(包括判断它是否为 nil )应该允许一次处理多个,因为它是线程安全的。

sync.RWMutex

读 / 写 mutex 可以同时被多个 reader 或者一个 writer 持有(writer 是指改写数据的某种东西):

mutex sync.RWMutex

...

func getCapitalCity(country string) string {
    mutex.RLock()
    if capitals != nil {
        country := capitals[country]
        mutex.RUnlock()
        return country
    }
    mutex.RUnlock()
    mutex.Lock()
    if capitals == nil {
        Bootstrap()
    }
    mutex.Unlock()
    return getCapitalCity(country)
}

现在代码变得复杂多了。第一部分使用读锁以允许多个 reader 同时读取 capitals 。在一开始 Bootstrap 还未完成,所以调用者会拿到 mutex.Lock() 并且做必要的初始化。当这部分结束,函数可以被再次调用来获取期望的值。这些都在 Bootstrap 函数已经返回之后。

最新的解决方法很显然维护起来比较难。幸运的是有一个内置的方法恰好帮我们应对这种情况……

sync.Once

once sync.Once

...

func getCapitalCity(country string) string {
    once.Do(bootstrap)
    return capital[country]
}

上面的代码比第一个简明版的解决方法(它并不正常工作)更加简单。可以保证 bootstrap 只会被调用一次,并且从 map 中读数据仅在 bootstrap 返回后才会被执行。

点赞以帮助别人发现这篇文章。如果你想得到新文章的更新,请关注我。

资源

The Go memory model specifies the conditions under which reads of a variable in one Goroutine can be guaranteed to …

golang.org

Package sync provides basic synchronization primitives such as mutual exclusion locks. Other than the Once and …

golang.org

保留部分版权

Golang Programming Concurrency Synchronization Software Development

喜欢读吗?给 Michał Łowicki 一些掌声吧。

简单鼓励下还是大喝采,根据你对这篇文章的喜欢程度鼓掌吧。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK