I have implemented an immutable hash map and accompanying STM container inspired by clojure's atom
, which is to say, something akin to C++'s std::unique_ptr
, in that it manages (but not necessarily owns) another object via a pointer, which can be passed around and swapped out, and only decrementing the reference count of the managed object instead of outright destructing it. When running some test code compiled with -O3
, I've started noticing incorrect results.
The code for the compare-and-swap looks something like this:
hashmap *update(stm *stm, update_callback cb, void *user_args)
{
while (1) {
lock(stm->mutex);
hashmap *current = stm->reference;
increment_ref_count(current); // Line 6
unlock(stm->mutex);
hashmap *aspirant = cb(current, user_args); // returns a new, thread local instance
increment_ref_count(aspirant);
// Position 1
lock(stm->mutex);
if (current == stm->reference) { // success, no modification while we were busy
stm->reference = aspirant;
increment_ref_count(aspirant); // stm now has a reference
unlock(stm->mutex);
// Position 2.1
decrement_ref_count(current); // release reference acquired at line 6
decrement_ref_count(current); // stm no longer has a reference
return aspirant;
} else { // reference was modified, loop and try again
unlock(stm->mutex);
// Position 2.2
decrement_ref_count(current); // release reference acquired at line 6
decrement_ref_count(aspirant); // ref_count now at zero, aspirant is free'd
}
}
}
increment_
& decrement_ref_count
in-/decrease the reference count of the hashmap atomically. If the count drops to zero as the result of a decrement, the hasmap will be free'd shortly after.
The challenge with designing an STM container for a reference counted pointer was mostly about making aquiring-the-reference-and-incrementing-the-counter atomic, which is why I'm using locks here.
As a test, I'm using the hashmap+STM to count occurences in a word list.
If I run the code as posted here, no race conditions occur. Now comes my problem: if I move the decrement_ref_count(current); // for line 6
out of the if/else
, away from Positions 2.1/2.2
(right after the second locked region) and put it at Position 1
(right before the second locked region), suddenly the word counts start being incorrect, and I have no idea why.
My argument is that a) I don't make any use of current
during the second critical region, and b) therefore it shouldn't matter if I release the reference before or after the compare-and-swap.
Obviously, I have a workaround/solution for the problem; simply leaving the decrement
's where they are now, but I would really like to understand why this happens.
Compiled with: gcc -Wall -Wcast-align -Wswitch-enum -Wswitch-default -Winit-self -pedantic -O3 -DNDEBUG -std=gnu11
on the "Windows Subsystem for Linux".