Как можно реализовать потокобезопасную оболочку для карт в Go с помощью блокировки?

Я пытаюсь обернуть общую карту (с interface{} как ключом и значением) как хранилище ключей и значений в памяти, которое я назвал MemStore. Но это не потокобезопасно, несмотря на то, что я использовал sync.RWMutex для блокировки доступа к базовой карте. Я убедился, что он отлично работает при использовании из одной горутины. Тем не менее, всего две параллельные горутины, обращающиеся к нему, приводят к panic: runtime error: invalid memory address or nil pointer dereference.

Что вызывает эту проблему и как правильно обеспечить потокобезопасность в Go? Хотя в этом примере каналы к одной горутине, взаимодействующей с картой, будут работать, я специально ищу решение, которое работает с явной блокировкой. Файл keyval.go:

package keyval

import "sync"

type MemStore struct {
    data map[interface{}]interface{}
    mutex sync.RWMutex
}

func NewMemStore() (MemStore) {
    m := MemStore{
        data: make(map[interface{}]interface{}),
        // mutex does not need initializing
    }
    return m
}

func (m MemStore) Set(key interface{}, value interface{}) (err error) {
    m.mutex.Lock()
    defer m.mutex.Unlock()

    if value != nil {
        m.data[key] = value
    } else {
        delete(m.data, key);
    }

    return nil
}

Файл keyval_test.go:

package keyval

import "testing"

func setN(store Store, N int, done chan<- struct{}) {
    for i := 0; i < N; i++ {
        store.Set(i, -i)
    }
    done <- struct{}{}
}

func BenchmarkMemStore(b *testing.B) {
    store := NewMemStore()
    done := make(chan struct{})
    b.ResetTimer()
    go setN(store, b.N, done)
    go setN(store, b.N, done)
    <-done
    <-done
}

Выход go test -bench .:

testing: warning: no tests to run
PASS
BenchmarkMemStore       panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x1 pc=0x80502eb]

goroutine 8 [running]:
runtime.panic(0x810f180, 0x821fc88)
        /usr/lib/go/src/pkg/runtime/panic.c:266 +0xac
github.com/pyramids/keyval.MemStore.Set(0x1852e4a0, 0x0, 0x0, 0x0, 0x0, ...)
        /home/steinb/go/src/github.com/pyramids/keyval/keyval.go:200 +0xb6
github.com/pyramids/keyval.(*MemStore).Set(0x1852efe0, 0x80f38c0, 0x19ff, 0x80f38c0, 0xffffe601, ...)
        /home/steinb/go/src/github.com/pyramids/keyval/keyval.go:1 +0xa1
github.com/pyramids/keyval.setN(0xb77b89d8, 0x1852efe0, 0x2710, 0x1851c090)
        /home/steinb/go/src/github.com/pyramids/keyval/memstore_test.go:69 +0x5b
created by github.com/pyramids/keyval.BenchmarkMemStore
        /home/steinb/go/src/github.com/pyramids/keyval/memstore_test.go:89 +0x116

goroutine 1 [chan receive]:
testing.(*B).run(0x1854a000, 0x3, 0xb76cdeb4, 0x1, 0x1, ...)
        /usr/lib/go/src/pkg/testing/benchmark.go:171 +0x4b
testing.RunBenchmarks(0x814d840, 0x821c828, 0x1, 0x1)
        /usr/lib/go/src/pkg/testing/benchmark.go:303 +0x464
testing.Main(0x814d840, 0x8222220, 0x0, 0x0, 0x821c828, ...)
        /usr/lib/go/src/pkg/testing/testing.go:411 +0x151
main.main()
        github.com/pyramids/keyval/_test/_testmain.go:47 +0x83

goroutine 3 [chan receive]:
github.com/pyramids/keyval.BenchmarkMemStore(0x1854a000)
        /home/steinb/go/src/github.com/pyramids/keyval/memstore_test.go:91 +0x188
testing.(*B).runN(0x1854a000, 0x2710)
        /usr/lib/go/src/pkg/testing/benchmark.go:119 +0x7a
testing.(*B).launch(0x1854a000)
        /usr/lib/go/src/pkg/testing/benchmark.go:207 +0x12c
created by testing.(*B).run
        /usr/lib/go/src/pkg/testing/benchmark.go:170 +0x32

goroutine 9 [runnable]:
sync.(*RWMutex).Lock(0x18582a64)
        /usr/lib/go/src/pkg/sync/rwmutex.go:72
github.com/pyramids/keyval.MemStore.Set(0x1852e4a0, 0x0, 0x0, 0x0, 0x0, ...)
        /home/steinb/go/src/github.com/pyramids/keyval/keyval.go:179 +0x5d
github.com/pyramids/keyval.(*MemStore).Set(0x1852efc0, 0x80f38c0, 0x2302, 0x80f38c0, 0xffffdcfe, ...)
        /home/steinb/go/src/github.com/pyramids/keyval/keyval.go:1 +0xa1
github.com/pyramids/keyval.setN(0xb77b89d8, 0x1852efc0, 0x2710, 0x1851c090)
        /home/steinb/go/src/github.com/pyramids/keyval/memstore_test.go:69 +0x5b
created by github.com/pyramids/keyval.BenchmarkMemStore
        /home/steinb/go/src/github.com/pyramids/keyval/memstore_test.go:90 +0x172
exit status 2
FAIL    github.com/pyramids/keyval      0.056s

person pyramids    schedule 10.05.2014    source источник


Ответы (1)


Эффективный переход

Указатели и значения

Правило относительно указателей и значений для приемников заключается в том, что методы значений могут вызываться для указателей и значений, но методы указателей могут вызываться только для указателей. Это связано с тем, что методы указателя могут изменять получателя; вызов их для копии значения приведет к отбрасыванию этих модификаций.

Чтобы явно изменить поле MemStore struct переменной mutex, используйте приемник указателя. Вы изменяете копию, невидимую для других процедур go. Например,

Файл keyval.go:

package keyval

import "sync"

type MemStore struct {
    data  map[interface{}]interface{}
    mutex sync.RWMutex
}

func NewMemStore() *MemStore {
    m := &MemStore{
        data: make(map[interface{}]interface{}),
        // mutex does not need initializing
    }
    return m
}

func (m *MemStore) Set(key interface{}, value interface{}) (err error) {
    m.mutex.Lock()
    defer m.mutex.Unlock()

    if value != nil {
        m.data[key] = value
    } else {
        delete(m.data, key)
    }

    return nil
}

Файл keyval_test.go:

package keyval

import "testing"

func setN(store *MemStore, N int, done chan<- struct{}) {
    for i := 0; i < N; i++ {
        store.Set(i, -i)
    }
    done <- struct{}{}
}

func BenchmarkMemStore(b *testing.B) {
    store := NewMemStore()
    done := make(chan struct{})
    b.ResetTimer()
    go setN(store, b.N, done)
    go setN(store, b.N, done)
    <-done
    <-done
}

Ориентир:

$ go test -v -bench=.
testing: warning: no tests to run
PASS
BenchmarkMemStore    1000000          1244 ns/op
ok      so/test 1.275s
$
person peterSO    schedule 10.05.2014