Closed pnegahdar closed 11 months ago
Error in my code, sorry!
Glad that you found the solution.
The documentation doesn't say much about how the function is expected to behave. e.g. would
LoadOrCompute('keyA', slowInitializer(a))
block outLoadOrCompute('keyB', slowinitializer(b))
?
When Compute
path of the LoadOrCompute
method kicks in, it locks the hash table bucket holding keyA. So, if keyB's hash code has the same lower bits as keyA's one, they will be stored in the same bucket and LoadOrCompute('keyB', slowinitializer(b))
would have to wait until Compute
on keyA finishes.
On the other hand, the number of buckets starts with 32 on an empty map and grows as the power of 2. In practice it means that a map has quite many buckets which lowers the chance of bucket collisions for Compute
calls.
Thanks for the information. Yeah I'm using the map as a "room" table for a chat like server (with a blocking initializer). The behavior of the locked-if-same-bucket is unreliable for these types of cases, I think the function should probably be clarified that it behaves that way (and that its dependent on the hash/bucket palcement). I would have probably been bitten by this if I hadn't dug in deeper.
I ended up writing a OnceMap type thing that uses singleflight to guard the initializer:
package utils
import (
"github.com/puzpuzpuz/xsync/v3"
"golang.org/x/sync/singleflight"
)
type OnceMap[T any] struct {
dat *xsync.MapOf[string, T]
group singleflight.Group
}
func (o *OnceMap[T]) LoadOrCompute(key string, fn func() (T, error)) (T, error) {
// Check cache
value, found := o.dat.Load(key)
if found {
return value, nil
}
_, err, _ := o.group.Do(key, func() (interface{}, error) {
// try again
if _, found := o.dat.Load(key); found {
return nil, nil
}
// compute
data, innerErr := fn()
if innerErr == nil {
o.dat.Store(key, data)
}
return nil, innerErr
})
value, _ = o.dat.Load(key)
return value, err
}
func NewOnceMap[T any]() *OnceMap[T] {
return &OnceMap[T]{
dat: xsync.NewMapOf[string, T](),
group: singleflight.Group{},
}
}
That's a good input. I'll update the godoc, so that the locking behavior is clear.
I was wondering if
xsync.MapOf
holds fine grained locks (per key) on LoadOrCompute(). The documentation doesn't say much about how the function is expected to behave. e.g. wouldLoadOrCompute('keyA', slowInitializer(a))
block outLoadOrCompute('keyB', slowinitializer(b))
?Wrote a quick test for it:
Output is:
I expected the third go routine (operating on
key-two
) not to get blocked by the initializers for the other keys. Reading the code, I guess it makes sense that it does get blocked.1) Is there a better way to achieve what I want to achieve ? I was hoping I'd avoid making my own map of sync.Once's
2) Would be helpful to clarify this behavior in the docs