Skip to content

Commit f55c03d

Browse files
committed
LockFreeLinkedSet: add initial implementation
1 parent 1d31751 commit f55c03d

File tree

5 files changed

+447
-0
lines changed

5 files changed

+447
-0
lines changed

lib/concurrent-edge.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
require 'concurrent/edge/future'
1010
require 'concurrent/edge/lock_free_stack'
1111
require 'concurrent/edge/atomic_markable_reference'
12+
require 'concurrent/edge/lock_free_linked_set'
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
require 'concurrent/edge/lock_free_linked_set/node'
2+
require 'concurrent/edge/lock_free_linked_set/window'
3+
4+
module Concurrent
5+
module Edge
6+
class LockFreeLinkedSet
7+
include Enumerable
8+
9+
# @!macro [attach] lock_free_linked_list_method_initialize
10+
#
11+
# @param [Fixnum] initial_size the size of the linked_list to initialize
12+
def initialize(initial_size = 0, val = nil)
13+
@head = Head.new
14+
15+
initial_size.times do
16+
val = block_given? ? yield : val
17+
18+
self.add val
19+
end
20+
end
21+
22+
# @!macro [attach] lock_free_linked_list_method_add
23+
#
24+
# Atomically adds the item to the set if it does not yet exist. Note:
25+
# internally the set uses `Object#hash` to compare equality of items,
26+
# meaning that Strings and other objects will be considered equal
27+
# despite being different objects.
28+
#
29+
# @param [Object] item the item you wish to insert
30+
#
31+
# @return [Boolean] `true` if successful. A `false` return indicates
32+
# that the item was already in the set.
33+
def add(item)
34+
loop do
35+
window = Window.find @head, item
36+
37+
pred, curr = window.pred, window.curr
38+
39+
# Item already in set
40+
if curr == item
41+
return false
42+
else
43+
node = Node.new item, curr
44+
45+
return true if pred.succ.compare_and_set curr, node, false, false
46+
end
47+
end
48+
end
49+
50+
# @!macro [attach] lock_free_linked_list_method_<<
51+
#
52+
# Atomically adds the item to the set if it does not yet exist.
53+
#
54+
# @param [Object] item the item you wish to insert
55+
#
56+
# @return [Oject] the set on which the :<< method was invoked
57+
def <<(item)
58+
self if add item
59+
end
60+
61+
# @!macro [attach] lock_free_linked_list_method_contains
62+
#
63+
# Atomically checks to see if the set contains an item. This method
64+
# compares equality based on the `Object#hash` method, meaning that the
65+
# hashed contents of an object is what determines equality instead of
66+
# `Object#object_id`
67+
#
68+
# @param [Object] item the item you to check for presence in the set
69+
#
70+
# @return [Boolean] whether or not the item is in the set
71+
def contains?(item)
72+
curr = @head
73+
74+
while curr < item
75+
curr = curr.next
76+
marked = curr.succ.marked?
77+
end
78+
79+
curr == item && !marked
80+
end
81+
82+
# @!macro [attach] lock_free_linked_list_method_remove
83+
#
84+
# Atomically attempts to remove an item, comparing using `Object#hash`.
85+
#
86+
# @param [Object] item the item you to remove from the set
87+
#
88+
# @return [Boolean] whether or not the item was removed from the set
89+
def remove(item)
90+
loop do
91+
window = Window.find @head, item
92+
pred, curr = window.pred, window.curr
93+
94+
if curr != item
95+
return false
96+
else
97+
succ = curr.next
98+
snip = curr.succ.compare_and_set succ, succ, false, true
99+
100+
next unless snip
101+
102+
pred.succ.compare_and_set curr, succ, false, false
103+
104+
return true
105+
end
106+
end
107+
end
108+
109+
# @!macro [attach] lock_free_linked_list_method_each
110+
#
111+
# An iterator to loop through the set.
112+
#
113+
# @param [Object] item the item you to remove from the set
114+
# @yeild [Object] each item in the set
115+
#
116+
# @return [Object] self: the linked set on which each was called
117+
def each
118+
return to_enum unless block_given?
119+
120+
curr = @head
121+
122+
until curr.last?
123+
curr = curr.next
124+
marked = curr.succ.marked?
125+
126+
yield curr.data if !marked
127+
end
128+
129+
self
130+
end
131+
end
132+
end
133+
end
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
require 'concurrent/edge/atomic_markable_reference'
2+
3+
module Concurrent
4+
module Edge
5+
class LockFreeLinkedSet
6+
class Node
7+
include Comparable
8+
9+
attr_reader :data, :succ, :key
10+
11+
def initialize(data = nil, succ = nil)
12+
@succ = AtomicMarkableReference.new(succ || Tail.new)
13+
@data = data
14+
@key = key_for data
15+
end
16+
17+
# Check to see if the node is the last in the list.
18+
def last?
19+
@succ.value.is_a? Tail
20+
end
21+
22+
# Next node in the list. Note: this is not the AtomicMarkableReference
23+
# of the next node, this is the actual Node itself.
24+
def next
25+
@succ.value
26+
end
27+
28+
# This method provides a unqiue key for the data which will be used for
29+
# ordering. This is configurable, and changes depending on how you wish
30+
# the nodes to be ordered.
31+
def key_for(data)
32+
data.hash
33+
end
34+
35+
# We use `Object#hash` as a way to enforce ordering on the nodes. This
36+
# can be configurable in the future; for example, you could enforce a
37+
# split-ordering on the nodes in the set.
38+
def <=>(other)
39+
@key <=> other.hash
40+
end
41+
end
42+
43+
# Internal sentinel node for the Tail. It is always greater than all
44+
# other nodes, and it is self-referential; meaning its successor is
45+
# a self-loop.
46+
class Tail < Node
47+
def initialize(data = nil, _succ = nil)
48+
@succ = AtomicMarkableReference.new self
49+
end
50+
51+
# Always greater than other nodes. This means that traversal will end
52+
# at the tail node since we are comparing node size in the traversal.
53+
def <=>(_other)
54+
1
55+
end
56+
end
57+
58+
59+
# Internal sentinel node for the Head of the set. Head is always smaller
60+
# than any other node.
61+
class Head < Node
62+
def <=>(_other)
63+
-1
64+
end
65+
end
66+
end
67+
end
68+
end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
module Concurrent
2+
module Edge
3+
class Window
4+
attr_accessor :pred, :curr
5+
6+
def initialize(pred, curr)
7+
@pred, @curr = pred, curr
8+
end
9+
10+
# This method is used to find a 'window' for which `add` and `remove`
11+
# methods can use to know where to add and remove from the list. However,
12+
# it has another responsibilility, which is to physically unlink any
13+
# nodes marked for removal in the set. This prevents adds/removes from
14+
# having to retraverse the list to physically unlink nodes.
15+
def self.find(head, item)
16+
loop do
17+
break_inner_loops = false
18+
pred = head
19+
curr = pred.next
20+
21+
loop do
22+
succ, marked = curr.succ.get
23+
24+
# Remove sequence of marked nodes
25+
while marked
26+
removed = pred.succ.compare_and_set curr, succ, false, false
27+
28+
# If could not remove node, try again
29+
break_inner_loops = true && break unless removed
30+
31+
curr = succ
32+
succ, marked = curr.succ.get
33+
end
34+
35+
break if break_inner_loops
36+
37+
# We have found a window
38+
return new pred, curr if curr >= item
39+
40+
pred = curr
41+
curr = succ
42+
end
43+
end
44+
end
45+
end
46+
end
47+
end

0 commit comments

Comments
 (0)