Skip to content

Commit 5b20837

Browse files
authored
Use Crystal::PointerLinkedList instead of Deque in Mutex (#15330)
Extracts the undocumented `Fiber::Waiting` struct from `WaitGroup` that acts as the node in a linked list, replacing a `Deque` to store the waiting fibers. The flat array doesn't have much impact on performance: we only reach the head or the tail once to dequeue/dequeue one fiber at a time. This however spares a number of GC allocations since the Deque has to be allocated plus its buffer that will have to be reallocated sometimes (and will only ever grow, never shrink).
1 parent 2458e35 commit 5b20837

File tree

3 files changed

+27
-19
lines changed

3 files changed

+27
-19
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
require "crystal/pointer_linked_list"
2+
3+
class Fiber
4+
# :nodoc:
5+
struct PointerLinkedListNode
6+
include Crystal::PointerLinkedList::Node
7+
8+
def initialize(@fiber : Fiber)
9+
end
10+
11+
def enqueue : Nil
12+
@fiber.enqueue
13+
end
14+
end
15+
end

src/mutex.cr

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require "fiber/pointer_linked_list_node"
12
require "crystal/spin_lock"
23

34
# A fiber-safe mutex.
@@ -22,7 +23,7 @@ class Mutex
2223
@state = Atomic(Int32).new(UNLOCKED)
2324
@mutex_fiber : Fiber?
2425
@lock_count = 0
25-
@queue = Deque(Fiber).new
26+
@queue = Crystal::PointerLinkedList(Fiber::PointerLinkedListNode).new
2627
@queue_count = Atomic(Int32).new(0)
2728
@lock = Crystal::SpinLock.new
2829

@@ -59,6 +60,8 @@ class Mutex
5960
loop do
6061
break if try_lock
6162

63+
waiting = Fiber::PointerLinkedListNode.new(Fiber.current)
64+
6265
@lock.sync do
6366
@queue_count.add(1)
6467

@@ -71,7 +74,7 @@ class Mutex
7174
end
7275
end
7376

74-
@queue.push Fiber.current
77+
@queue.push pointerof(waiting)
7578
end
7679

7780
Fiber.suspend
@@ -116,17 +119,18 @@ class Mutex
116119
return
117120
end
118121

119-
fiber = nil
122+
waiting = nil
120123
@lock.sync do
121124
if @queue_count.get == 0
122125
return
123126
end
124127

125-
if fiber = @queue.shift?
128+
if waiting = @queue.shift?
126129
@queue_count.add(-1)
127130
end
128131
end
129-
fiber.enqueue if fiber
132+
133+
waiting.try(&.value.enqueue)
130134
end
131135

132136
def synchronize(&)

src/wait_group.cr

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
require "fiber"
2+
require "fiber/pointer_linked_list_node"
23
require "crystal/spin_lock"
3-
require "crystal/pointer_linked_list"
44

55
# Suspend execution until a collection of fibers are finished.
66
#
@@ -31,17 +31,6 @@ require "crystal/pointer_linked_list"
3131
# wg.wait
3232
# ```
3333
class WaitGroup
34-
private struct Waiting
35-
include Crystal::PointerLinkedList::Node
36-
37-
def initialize(@fiber : Fiber)
38-
end
39-
40-
def enqueue : Nil
41-
@fiber.enqueue
42-
end
43-
end
44-
4534
# Yields a `WaitGroup` instance and waits at the end of the block for all of
4635
# the work enqueued inside it to complete.
4736
#
@@ -59,7 +48,7 @@ class WaitGroup
5948
end
6049

6150
def initialize(n : Int32 = 0)
62-
@waiting = Crystal::PointerLinkedList(Waiting).new
51+
@waiting = Crystal::PointerLinkedList(Fiber::PointerLinkedListNode).new
6352
@lock = Crystal::SpinLock.new
6453
@counter = Atomic(Int32).new(n)
6554
end
@@ -128,7 +117,7 @@ class WaitGroup
128117
def wait : Nil
129118
return if done?
130119

131-
waiting = Waiting.new(Fiber.current)
120+
waiting = Fiber::PointerLinkedListNode.new(Fiber.current)
132121

133122
@lock.sync do
134123
# must check again to avoid a race condition where #done may have

0 commit comments

Comments
 (0)