Skip to content

Commit 4ce3c14

Browse files
committed
Merge pull request #335 from ianks/lock-free-linked-set
LockFreeLinkedSet: add initial implementation
2 parents 931f208 + 10ddb2a commit 4ce3c14

File tree

5 files changed

+471
-0
lines changed

5 files changed

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

0 commit comments

Comments
 (0)