Гарантии параллелизма канала - PullRequest
0 голосов
/ 07 мая 2018

Я пишу памятку, безопасную для параллелизма:

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, которое используется сигнализировать другим goroutines, что вычисление для ключа ожидает или сделано. Это позволяет избежать дублирования вычислений (вызовов c.f(key)) для одного и того же ключа.

Мой вопрос касается гарантий этого кода; гарантируя, что компьютерная программа закрывает канал после записи в c.memo, гарантирует ли это, что другие программы, которые обращаются к c.memo[key] после блокирующего вызова <-c.done[key], гарантированно увидят результат вычисления?

1 Ответ

0 голосов
/ 07 мая 2018

Краткий ответ: да .

Мы можем упростить часть кода, чтобы понять суть почему. Рассмотрим вашу Mu структуру:

type Mu struct {
    memo int
    done chan bool
}

Теперь мы можем определить 2 функции, compute и read

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 - сложная вычислительная задача (которую мы можем смоделировать, поспав некоторое время)

Теперь в основной функции мы запускаем новую 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)
}

Во всех трех случаях мы распечатываем результат задачи вычисления.

Рабочий код здесь

Когда вы «закрываете» канал в go, все операторы, которые ожидают результата канала (включая операторы, которые выполняются после его закрытия), блокируются. Таким образом, при условии, что место only , от которого закрывается канал, является местом, где вычисляется значение памятки, у вас будет эта гарантия.

Единственное место, где вы должны быть осторожны, это убедиться, что этот канал не закрыт нигде в вашем коде.

...