2

Now i have a map with only one write/delete goroutine and many read goroutines, there are some solutions upon Map with concurrent access, such as RWMutex, sync.map, concurrent-map, sync.atomic, sync.Value, what's the best choice for me?

RWMutex's read lock is a little redundant

sync.map and concurrent-map focus on many write goroutines

白栋天
  • 152
  • 2
  • 11
  • The right choice depends on a lot of factors and no choice is optimal in al circumstances. Go with a RWMutex. – Volker Sep 26 '18 at 08:17
  • So in my circumstance (only one write/delete goroutine and thousands of read goroutines), the RWMutex is a good choice? – 白栋天 Sep 26 '18 at 08:29
  • 1
    It does not matter how many read- or write-goroutines you have but how many actual reads/write. Thousands of read-goroutines doing one read per day and one write-goroutine writing once ever 300ns is probably not what you have but it illustrates the problem. You first must get your metrics straight. – Volker Sep 26 '18 at 08:35
  • 3
    A RWMutex is a good solution as it is simple. If you have 1e8 reads/sec and 1e4 writes/s then a simple RWMutex protecting a single map might be still optimal with respect to code simplicity but suboptimal in respect to throughput and lock contention. You first have to know what is important to you. – Volker Sep 26 '18 at 08:36
  • Actually, the amount of read ops is tens of thousands of times the number of write/delete ops. The second one's value per day is proportional to the number of users which may grows from hundreds to millions, and throughput is very improtant – 白栋天 Sep 26 '18 at 08:42
  • The best solution is probably using a `RWMutex`. Wrap it around a custom type if you want, so that you can have simple `Get(string)` and `Set(string, string)` (for example) methods which take care of the locking, which should make it less redundant. Then try it out on your application and see if the map actually _is_ a bottleneck (pprof may come in handy). If so, try out other solutions (such as `sync.Map`) – morganbaz Sep 26 '18 at 09:00
  • Then builtin map is not the right data structure if you need basically unlimited scalability. Such questions are hard (if the requirements are real and not just imagined) and cannot be answered on SO. – Volker Sep 26 '18 at 09:10
  • If the write/delete goroutine only update and do not delete key, is it a best choice that using builtin map with StorePointer(sync.atomic) when update is needed – 白栋天 Sep 26 '18 at 09:20

1 Answers1

2

Your question is a little vague - so I'll break it down.

What form of concurrent access should I use for a map?

The choice depends on the performance you require from the map. I would opt for a simple mutex (or a RWMutex) based approach.

Granted, you can get better performance from a concurrent map. sync.Mutex locks all of a maps buckets, whereas in a concurrent map, each bucket has it's own sync.Mutex.

Again - it all depends on the scale of your program and the performance you require.

How would I use a mutex for concurrent access?

To ensure the map is being used correctly, you can wrap this in a struct.

type Store struct {
    Data              map[T]T
}

This a more object-oriented solution, but it allows us to make sure any read/writes are performed concurrently. As well as this, we can easily store other information that may be useful for debugging or security, such as author.

Now, we would implement this with a set of methods like so:

mux sync.Mutex

// New initialises a Store type with an empty map
func New(t, h uint) *Store {
    return &Store{
        Data:     map[T]T{},
    }
}

// Insert adds a new key i to the store and places the value of x at this location
// If there is an error, this is returned - if not, this is nil
func (s *Store) Insert(i, x T) error {
    mux.Lock()
    defer mux.Unlock()
    _, ok := s.Data[i]
    if ok {
        return fmt.Errorf("index %s already exists; use update", i)
    }
    s.Data[i] = x
    return nil
}

// Update changes the value found at key i to x
// If there is an error, this is returned - if not, this is nil
func (s *Store) Update(i, x T) error {
    mux.Lock()
    defer mux.Unlock()
    _, ok := s.Data[i]
    if !ok {
        return fmt.Errorf("value at index %s does not exist; use insert", i)
    }
    s.Data[i] = x
    return nil
}

// Fetch returns the value found at index i in the store
// If there is an error, this is returned - if not, this is nil
func (s *Store) Fetch(i T) (T, error) {
    mux.Lock()
    defer mux.Unlock()
    v, ok := s.Data[i]
    if !ok {
        return "", fmt.Errorf("no value for key %s exists", i)
    }
    return v, nil
}

// Delete removes the index i from store
// If there is an error, this is returned - if not, this is nil
func (s *Store) Delete(i T) (T, error) {
    mux.Lock()
    defer mux.Unlock()
    v, ok := s.Data[i]
    if !ok {
        return "", fmt.Errorf("index %s already empty", i)
    }
    delete(s.Data, i)
    return v, nil
}

In my solution, I've used a simple sync.Mutex - but you can simply change this code to accommodate RWMutex.

I recommend you take a look at How to use RWMutex in Golang?.

psti
  • 157
  • 2
  • 5
  • 14
  • As describe in comments above, now i have a write/delete goroutine may have hundreds or millions ops per day, and many read goroutines whose ops amount may be tens of thousands times of the write/delete ops. Is the RWmutex is better than sync.map on throughput ? – 白栋天 Sep 26 '18 at 09:14
  • @白栋天 Try my above solution with a RWMutex and benchmark it for the number of operations you're expecting. There are mixed views on the performance of sync.Map, so it might be worth doing a bit of research first: https://medium.com/@deckarep/the-new-kid-in-town-gos-sync-map-de24a6bf7c2c – psti Sep 26 '18 at 10:01