puzpuzpuz / xsync

Concurrent data structures for Go
Apache License 2.0
1.13k stars 40 forks source link

[xsync.MapOf] Atomically delete a value based on value's state? #128

Closed alberto-miranda closed 6 months ago

alberto-miranda commented 7 months ago

Hi! We are using xsync.MapOf for a couple of projects (great package btw :wink: ) and we recently have a new use case where we would need to delete a value atomically if and only if such value meets certain conditions. Basically something like this:

func main() {
    ms := xsync.NewMapOf[int, []string]()

    id := 42

    // Delete the value atomically if and only if someCondition and someOtherCondition are true
    v, deleted := ms.DeleteIf(id, func(value []string) bool {
        if someCondition(value) && someOtherCondition(value) {
            return true
        }

        return false
    })

    // If the value was deleted, we can use it safely since it's not in the map anymore
    if deleted {
        fmt.Printf("v: %v\n", v)
    }
}

Is there a way to do this with MapOf? If there isn't and the implementation is not very complex, would you accept a contribution for this?

puzpuzpuz commented 7 months ago

Hello,

You can use Compute for this purpose: if it's delete return value is true, the key will be deleted. See the Delete an existing value part of the doc example.

alberto-miranda commented 7 months ago

Thanks for the prompt response. Is a strict ordering guaranteed between competing calls to Compute() and Load() in different goroutines? We wrote a small test to validate our implementation, where a goroutine tries to Load() a key concurrently with a deletion, and it looks like Load() is able to find the value while the valueFn passed to Compute is being executed. We assumed that once valueFn started its execution, other goroutines would be blocked until it completed, but maybe we were wrong about this?

puzpuzpuz commented 7 months ago

Writes, e.g. Compute or Store or Delete, never block reads, e.g. Load or Range. That's the reason why MapOf reads scale in terms of core/goroutine count. In your case, you could try using Compute for both reads and writes - readers may simply read the key and do something about it while writers will be deleting it based on a condition. Just make sure to do this in the function provided to the Compute call - otherwise, as soon as your code leaves the function, a concurrent reader/writer may do something with the key.

alberto-miranda commented 6 months ago

Great, thanks, we will attempt what you propose. It will not be as performant as it could, but it will be consistent which is what we need right now.