go 圣经杂乱的笔记 -- lock
12 July 2018

go 不支持重入锁

解决方法
一个通用的解决方案是将一个函数分离为多个函数,比如我们把 Deposit 分离成两个:一个不导出的函数 deposit,这个函数假设锁总是会被保持并去做实际的操作,另一个是导出的函数 Deposit,这个函数会调用 deposit,但在调用前会先去获取锁。同理我们可以将 Withdraw 也表示成这种形式

var (
        mu      sync.Mutex // guards balance
        balance int
)

func Deposit(amount int) {
        mu.Lock()
        balance = balance + amount
        mu.Unlock()
}

func Balance() int {
        mu.Lock()
        b := balance
        mu.Unlock()
        return b
}

func Withdraw(amount int) bool {
        mu.Lock()              // 这里锁了第一次
        defer mu.Unlock()
        Deposit(-amount)       // 这里锁了第二次
        if Balance() < 0 {
                Deposit(amount)
                return false // insufficient funds
        }
        return true
}

// fatal error: all goroutines are asleep - deadlock!
func Withdraw(amount int) bool {
        mu.Lock()
        defer mu.Unlock()
        deposit(-amount)
        if balance < 0 {
                deposit(amount)
                return false // insufficient funds
        }
        return true
}

func Deposit(amount int) {
        mu.Lock()
        defer mu.Unlock()
        deposit(amount)
}

func Balance() int {
        mu.Lock()
        defer mu.Unlock()
        return balance
}

// This function requires that the lock be held.
func deposit(amount int) { balance += amount }

还有另外一个方法,就是 Withdraw 方法申请的锁不要和 Deposit 是一个同一个,即定义 var mu1 sync.Mutex, var mu2 sync.Mutex 两个锁

并发顺序

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

y:0 x:1
x:0 y:1
x:1 y:1
y:1 x:1

第四行可以被解释为执行顺序 A1,B1,A2,B2 或者 B1,A1,A2,B2 的执行结果。 然而实际的运行时还是有些情况让我们有点惊讶:

x:0 y:0
y:0 x:0

但是根据所使用的编译器,CPU,或者其它很多影响因子,这两种情况也是有可能发生的。那么这两种情况要怎么解释呢?

在一个独立的 goroutine 中,每一个语句的执行顺序是可以被保证的;也就是说 goroutine 是顺序连贯的。但是在不使用 channel 且不使用 mutex 这样的显式同步操作时,我们就没法保证事件在不同的 goroutine 中看到的执行顺序是一致的了。尽管 goroutine A 中一定需要观察到 x=1 执行成功之后才会去读取 y,但它没法确保自己观察得到 goroutine B 中对 y 的写入,所以 A 还可能会打印出 y 的一个旧版的值。

尽管去理解并发的一种尝试是去将其运行理解为不同 goroutine 语句的交错执行,但看看上面的例子,这已经不是现代的编译器和 cpu 的工作方式了。因为赋值和打印指向不同的变量,编译器可能会断定两条语句的顺序不会影响执行结果,并且会交换两个语句的执行顺序。如果两个 goroutine 在不同的 CPU 上执行,每一个核心有自己的缓存,这样一个 goroutine 的写入对于其它 goroutine 的 Print,在主存同步之前就是不可见的了。

所有并发的问题都可以用一致的、简单的既定的模式来规避。所以可能的话,将变量限定在 goroutine 内部;如果是多个 goroutine 都需要访问的变量,使用互斥条件来访问。

竞争条件检测

在 go build,go run 或者 go test 命令后面加上 -race 的 flag

memo 5

func incomingURLs() []string {
        return []string{
                "https://golang.org",
                "https://godoc.org",
                "https://play.golang.org",
                "http://gopl.io",
                "https://golang.org",
                "https://godoc.org",
                "https://play.golang.org",
                "http://gopl.io",
        }
}

func httpGetBody(url string) (interface{}, error) {
        resp, err := http.Get(url)
        if err != nil {
                return nil, err
        }
        defer resp.Body.Close()
        return ioutil.ReadAll(resp.Body)
}

func main() {
        m := New(httpGetBody)
        defer m.Close()

        for _, url := range incomingURLs() {
                start := time.Now()
                value, err := m.Get(url)
                if err != nil {
                        log.Print(err)
                        continue
                }
                fmt.Printf("%s, %s, %d bytes\n",
                        url, time.Since(start), len(value.([]byte)))
        }

}

type Func func(key string) (interface{}, error)

type result struct {
        value interface{}
        err   error
}

type entry struct {
        res   result
        ready chan bool
}

type request struct {
        key      string
        response chan<- result
}

type Memo struct {
        requests chan request
}

func New(f Func) *Memo {
        memo := &Memo{
                requests: make(chan request),
        }

        go memo.server(f)
        return memo
}

func (memo *Memo) Get(key string) (interface{}, error) {
        response := make(chan result)
        memo.requests <- request{
                key:      key,
                response: response,
        }

        res := <-response
        return res.value, res.err
}

func (memo *Memo) Close() {
        close(memo.requests)
}

func (memo *Memo) server(f Func) {
        cache := make(map[string]*entry)

        for req := range memo.requests {
                e := cache[req.key]
                if e == nil {
                        e = &entry{
                                ready: make(chan bool),
                        }

                        cache[req.key] = e
                        go e.call(f, req.key)
                }

                go e.deliver(req.response)
        }
}

func (e *entry) call(f Func, key string) {
        e.res.value, e.res.err = f(key)
        close(e.ready) // 这里要直接 close 掉
}

func (e *entry) deliver(response chan<- result) {
        <-e.ready
        response <- e.res
}
func main() {
        done := make(chan bool)
        go func() {
                close(done)
        }()
        <-done
        <-done
        <-done
        fmt.Printf("%v\n", "done")
}

这里是实现一个 server 的一个套路,通常是 New 操作,里面跑了一个 goroutine,这个 goroutine 在等待一个 requests 的 channel,一旦 requests channel 有数据进来,就在里面在开启再开启一个 goroutine

type request {
    xxx
    response chan xxx  // chan 类型
}

requests := make(chan request)

func Get() {
    response := make(chan xxx)
    requests <- &requst {
        xxx,
        response: response,
    }
    res := <- response
}

for req := range requests {
        go xxx(req)
}

func xxx(req) {
    // ...
    req.response <- res
}