Skip to content

Commit ad3a103

Browse files
committed
Prevent lost updates in concurrent writes
1 parent 818f4dd commit ad3a103

4 files changed

Lines changed: 157 additions & 15 deletions

File tree

lib/abit.ex

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,7 @@ defmodule Abit do
100100
defp do_union(ref_a, _, 0), do: ref_a
101101

102102
defp do_union(ref_a, ref_b, index) do
103-
unioned_value = :atomics.get(ref_a, index) ||| :atomics.get(ref_b, index)
104-
105-
:atomics.put(ref_a, index, unioned_value)
103+
update_atomic(ref_a, ref_b, index, :union)
106104

107105
do_union(ref_a, ref_b, index - 1)
108106
end
@@ -124,9 +122,7 @@ defmodule Abit do
124122
defp do_intersect(ref_a, _, 0), do: ref_a
125123

126124
defp do_intersect(ref_a, ref_b, index) do
127-
intersected_value = :atomics.get(ref_a, index) &&& :atomics.get(ref_b, index)
128-
129-
:atomics.put(ref_a, index, intersected_value)
125+
update_atomic(ref_a, ref_b, index, :intersect)
130126

131127
do_intersect(ref_a, ref_b, index - 1)
132128
end
@@ -150,9 +146,7 @@ defmodule Abit do
150146
defp do_difference(ref_a, _, 0), do: ref_a
151147

152148
defp do_difference(ref_a, ref_b, index) do
153-
diff_value = :atomics.get(ref_a, index) &&& bnot(:atomics.get(ref_b, index))
154-
155-
:atomics.put(ref_a, index, diff_value)
149+
update_atomic(ref_a, ref_b, index, :difference)
156150

157151
do_difference(ref_a, ref_b, index - 1)
158152
end
@@ -175,13 +169,35 @@ defmodule Abit do
175169
defp do_symmetric_difference(ref_a, _, 0), do: ref_a
176170

177171
defp do_symmetric_difference(ref_a, ref_b, index) do
178-
xor_value = :atomics.get(ref_a, index) |> bxor(:atomics.get(ref_b, index))
179-
180-
:atomics.put(ref_a, index, xor_value)
172+
update_atomic(ref_a, ref_b, index, :symmetric_difference)
181173

182174
do_symmetric_difference(ref_a, ref_b, index - 1)
183175
end
184176

177+
defp update_atomic(ref_a, ref_b, index, operation) do
178+
current_value = :atomics.get(ref_a, index)
179+
180+
do_update_atomic(ref_a, ref_b, index, operation, current_value)
181+
end
182+
183+
defp do_update_atomic(ref_a, ref_b, index, operation, current_value) do
184+
other_value = :atomics.get(ref_b, index)
185+
next_value = apply_bitwise_operation(operation, current_value, other_value)
186+
187+
case :atomics.compare_exchange(ref_a, index, current_value, next_value) do
188+
:ok ->
189+
:ok
190+
191+
new_current_value ->
192+
do_update_atomic(ref_a, ref_b, index, operation, new_current_value)
193+
end
194+
end
195+
196+
defp apply_bitwise_operation(:union, left, right), do: left ||| right
197+
defp apply_bitwise_operation(:intersect, left, right), do: left &&& right
198+
defp apply_bitwise_operation(:difference, left, right), do: left &&& bnot(right)
199+
defp apply_bitwise_operation(:symmetric_difference, left, right), do: bxor(left, right)
200+
185201
@doc """
186202
Inverts all bits in the signed atomics reference `ref` using bitwise NOT.
187203

lib/abit/counter.ex

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ defmodule Abit.Counter do
155155
end
156156

157157
def put(
158-
%Counter{atomics_ref: atomics_ref, signed: signed, counters_bit_size: counters_bit_size},
158+
%Counter{atomics_ref: atomics_ref, counters_bit_size: counters_bit_size} = counter,
159159
index,
160160
value
161161
)
@@ -164,12 +164,36 @@ defmodule Abit.Counter do
164164

165165
atomics_value = :atomics.get(atomics_ref, atomics_index)
166166

167+
do_put(counter, index, value, atomics_index, bit_index, atomics_value)
168+
end
169+
170+
defp do_put(
171+
%Counter{
172+
atomics_ref: atomics_ref,
173+
signed: signed,
174+
counters_bit_size: counters_bit_size
175+
} = counter,
176+
index,
177+
value,
178+
atomics_index,
179+
bit_index,
180+
atomics_value
181+
) do
167182
{final_counter_value, <<next_atomics_value::64>>} =
168183
put_value(signed, counters_bit_size, bit_index, <<atomics_value::64>>, value)
169184

170-
:atomics.put(atomics_ref, atomics_index, next_atomics_value)
185+
case :atomics.compare_exchange(
186+
atomics_ref,
187+
atomics_index,
188+
atomics_value,
189+
next_atomics_value
190+
) do
191+
:ok ->
192+
{:ok, {index, final_counter_value}}
171193

172-
{:ok, {index, final_counter_value}}
194+
new_atomics_value ->
195+
do_put(counter, index, value, atomics_index, bit_index, new_atomics_value)
196+
end
173197
end
174198

175199
@doc """

test/abit/counter_test.exs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,21 @@ defmodule Abit.CounterTest do
144144
for i <- 1..8, do: assert Counter.get(counter, i) == 1
145145
end
146146

147+
test "put/3 preserves concurrent writes to counters in the same atomics integer" do
148+
for _trial <- 1..100 do
149+
counter = Counter.new(8, 8, signed: false)
150+
151+
operations =
152+
for index <- 0..7 do
153+
fn -> Counter.put(counter, index, index + 1) end
154+
end
155+
156+
run_simultaneously(operations)
157+
158+
assert Enum.to_list(counter) == Enum.to_list(1..8)
159+
end
160+
end
161+
147162
test "Enumerable protocol implementation" do
148163
counter = Counter.new(10, 8)
149164
Counter.put(counter, 1, 42)
@@ -192,4 +207,30 @@ defmodule Abit.CounterTest do
192207
assert Counter.get(c, 0) == 10
193208
end
194209
end
210+
211+
defp run_simultaneously(operations) do
212+
parent = self()
213+
gate = make_ref()
214+
215+
tasks =
216+
Enum.map(operations, fn operation ->
217+
Task.async(fn ->
218+
send(parent, {gate, :ready, self()})
219+
220+
receive do
221+
{^gate, :go} -> operation.()
222+
end
223+
end)
224+
end)
225+
226+
pids =
227+
for _task <- tasks do
228+
receive do
229+
{^gate, :ready, pid} -> pid
230+
end
231+
end
232+
233+
Enum.each(pids, &send(&1, {gate, :go}))
234+
Enum.each(tasks, &Task.await/1)
235+
end
195236
end

test/abit_test.exs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,41 @@ defmodule AbitTest do
170170
end
171171
end
172172

173+
test "binary operations preserve concurrent updates to the same atomic element" do
174+
operation_cases = [
175+
{:union, 0, 0xFF},
176+
{:intersect, 0xFF, 0},
177+
{:difference, 0xFF, 0},
178+
{:symmetric_difference, 0, 0xFF}
179+
]
180+
181+
for {operation, initial_value, expected_value} <- operation_cases,
182+
_trial <- 1..100 do
183+
ref_l = :atomics.new(1, signed: false)
184+
:atomics.put(ref_l, 1, initial_value)
185+
186+
operations =
187+
for bit <- 0..7 do
188+
ref_r = :atomics.new(1, signed: false)
189+
bit_value = Bitwise.bsl(1, bit)
190+
191+
right_value =
192+
case operation do
193+
:intersect -> Bitwise.bxor(0xFF, bit_value)
194+
_other -> bit_value
195+
end
196+
197+
:atomics.put(ref_r, 1, right_value)
198+
199+
fn -> apply(Abit, operation, [ref_l, ref_r]) end
200+
end
201+
202+
run_simultaneously(operations)
203+
204+
assert :atomics.get(ref_l, 1) == expected_value
205+
end
206+
end
207+
173208
test "invert atomics bit arrays returns reference" do
174209
ref = :atomics.new(2, signed: true)
175210

@@ -350,4 +385,30 @@ defmodule AbitTest do
350385
assert Abit.set_bits_count(ref) == 96
351386
end
352387
end
388+
389+
defp run_simultaneously(operations) do
390+
parent = self()
391+
gate = make_ref()
392+
393+
tasks =
394+
Enum.map(operations, fn operation ->
395+
Task.async(fn ->
396+
send(parent, {gate, :ready, self()})
397+
398+
receive do
399+
{^gate, :go} -> operation.()
400+
end
401+
end)
402+
end)
403+
404+
pids =
405+
for _task <- tasks do
406+
receive do
407+
{^gate, :ready, pid} -> pid
408+
end
409+
end
410+
411+
Enum.each(pids, &send(&1, {gate, :go}))
412+
Enum.each(tasks, &Task.await/1)
413+
end
353414
end

0 commit comments

Comments
 (0)