通道并发保证

时间:2018-05-07 05:37:43

标签: caching go concurrency channel memoization

我正在写一份并发安全的备忘录:

package mu

import (
    "sync"
)

// Func represents a memoizable function, operating on a string key, to use with a Mu
type Func func(key string) interface{}

// Mu is a cache that memoizes results of an expensive computation
//
// It has a traditional implementation using mutexes.
type Mu struct {
    // guards done
    mu   sync.RWMutex
    done map[string]chan bool
    memo map[string]interface{}
    f    Func
}

// Get a string key if it exists, otherwise computes the value and caches it.
//
// Returns the value and whether or not the key existed.
func (c *Mu) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    _, ok := c.done[key]
    c.mu.RUnlock()
    if ok {
        return c.get(key), true
    }

    c.mu.Lock()
    _, ok = c.done[key]
    if ok {
        c.mu.Unlock()
    } else {
        c.done[key] = make(chan bool)
        c.mu.Unlock()

        v := c.f(key)
        c.memo[key] = v

        close(c.done[key])
    }
    return c.get(key), ok
}

// get returns the value of key, blocking on an existing computation
func (c *Mu) get(key string) interface{} {
    <-c.done[key]
    v, _ := c.memo[key]
    return v
}

正如您所看到的,有一个互斥锁保护done字段,使用了 向其他goroutine发信号通知一个密钥的计算正在等待或完成。这可以避免对同一个密钥进行重复计算(调用c.f(key))。

我的问题是关于此代码的保证;通过确保计算goroutine在写入c.memo后关闭通道,这可以保证在阻止调用c.memo[key]后访问<-c.done[key]的其他goroutine可以保证看到结果计算

1 个答案:

答案 0 :(得分:1)

简短的回答是

我们可以简化一些代码以了解原因。考虑一下Mu结构:

type Mu struct {
    memo int
    done chan bool
}

我们现在可以定义2个函数,computeread

func compute(r *Mu) {
    time.Sleep(2 * time.Second)
    r.memo = 42
    close(r.done)
}

func read(r *Mu) {
    <-r.done
    fmt.Println("Read value: ", r.memo)
}

在这里,compute是一项计算量很大的任务(我们可以通过睡眠模拟一段时间)

现在,在main函数中,我们开始一个新的compute go例程,并定期启动一些read go例程:

func main() {
    r := &Mu{}
    r.done = make(chan bool)
    go compute(r)

    // this one starts immediately
    go read(r)
    time.Sleep(time.Second)

    // this one starts in the middle of computation
    go read(r)
    time.Sleep(2*time.Second)

    // this one starts after the computation is complete
    go read(r)

    // This is to prevent the program from terminating immediately
    time.Sleep(3 * time.Second)
}

在所有三种情况下,我们都会打印出计算任务的结果。

  

工作代码here

当你在go中“关闭”一个通道时,所有等待通道结果的语句(包括在它关闭后执行的语句)都将被阻塞。因此,如果通道关闭的 only 位置是计算备忘录值的位置,那么您将获得该保证。

唯一需要注意的地方是确保代码中的其他任何地方都没有关闭此频道。