atomic-ruby 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -1
- data/README.md +21 -21
- data/ext/atomic_ruby/atomic_ruby.c +4 -4
- data/ext/atomic_ruby/atomic_ruby.h +1 -1
- data/lib/atomic-ruby/atom.rb +86 -0
- data/lib/atomic-ruby/atomic_boolean.rb +117 -0
- data/lib/atomic-ruby/atomic_count_down_latch.rb +129 -0
- data/lib/atomic-ruby/atomic_thread_pool.rb +172 -3
- data/lib/atomic-ruby/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b52931f98d46947d0d2f4dccffbe51e771c619d61b54457a223162be41624353
|
|
4
|
+
data.tar.gz: 3c91a27d99c3eb4d2a04f668bc5ebbc19c1c60b6a9fc688812df96c20bed37a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3f6d04175439390f6b9f29d91bf3cfad5b95018f7363e3cb341c7512857008e285d3a462363108cb54e613ed898153e157d7c41b1e113d92c38053a52e5d5be
|
|
7
|
+
data.tar.gz: aadb6f2b22fa068d8599b1e0aea30bc6e52d4b97198c4e4078e465d172f168b8548c9ab2f4bd174298aa2f5d02cd421c868a896c4b0d53689706edeb2414ff35
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.10.0] - 2025-11-09
|
|
4
|
+
|
|
5
|
+
- Add `AtomicThreadPool#active_count`
|
|
6
|
+
- Make native extension methods private
|
|
7
|
+
- Add YARD documentation and inline RBS type signatures
|
|
8
|
+
- Replace ruby 3.5 references with 4.0
|
|
9
|
+
|
|
3
10
|
## [0.9.0] - 2025-11-05
|
|
4
11
|
|
|
5
12
|
- Switch `AtomicThreadPool` back to atomics now that Ractor safety is lazy
|
|
@@ -12,7 +19,7 @@
|
|
|
12
19
|
|
|
13
20
|
## [0.8.0] - 2025-11-01
|
|
14
21
|
|
|
15
|
-
- Fix Ractor safety
|
|
22
|
+
- Fix Ractor safety
|
|
16
23
|
- Make `ArgumentError` messages consistent
|
|
17
24
|
- Implement write barriers for `Atom`
|
|
18
25
|
|
data/README.md
CHANGED
|
@@ -98,7 +98,7 @@ p latch.count #=> 0
|
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
> [!NOTE]
|
|
101
|
-
> `Atom`, `AtomicBoolean`, and `AtomicCountDownLatch` are Ractor-safe in Ruby
|
|
101
|
+
> `Atom`, `AtomicBoolean`, and `AtomicCountDownLatch` are Ractor-safe in Ruby 4.0+.
|
|
102
102
|
|
|
103
103
|
## Benchmarks
|
|
104
104
|
|
|
@@ -210,9 +210,9 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
|
|
|
210
210
|
```
|
|
211
211
|
> bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
|
|
212
212
|
|
|
213
|
-
ruby version: ruby
|
|
213
|
+
ruby version: ruby 4.0.0dev (2025-11-08T15:08:09Z master 75d25a42e6) +YJIT +PRISM [arm64-darwin25]
|
|
214
214
|
concurrent-ruby version: 1.3.5
|
|
215
|
-
atomic-ruby version: 0.
|
|
215
|
+
atomic-ruby version: 0.10.0
|
|
216
216
|
|
|
217
217
|
Balances:
|
|
218
218
|
Synchronized Bank Account Balance: 975
|
|
@@ -220,9 +220,9 @@ Concurrent Ruby Atomic Bank Account Balance: 975
|
|
|
220
220
|
Atomic Ruby Atomic Bank Account Balance: 975
|
|
221
221
|
|
|
222
222
|
Benchmark Results:
|
|
223
|
-
Synchronized Bank Account: 5.
|
|
224
|
-
Concurrent Ruby Atomic Bank Account: 5.
|
|
225
|
-
Atomic Ruby Atomic Bank Account: 5.
|
|
223
|
+
Synchronized Bank Account: 5.112382 seconds
|
|
224
|
+
Concurrent Ruby Atomic Bank Account: 5.113139 seconds
|
|
225
|
+
Atomic Ruby Atomic Bank Account: 5.101891 seconds
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
</details>
|
|
@@ -301,29 +301,29 @@ end
|
|
|
301
301
|
```
|
|
302
302
|
> bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
|
|
303
303
|
|
|
304
|
-
ruby version: ruby
|
|
304
|
+
ruby version: ruby 4.0.0dev (2025-11-08T15:08:09Z master 75d25a42e6) +YJIT +PRISM [arm64-darwin25]
|
|
305
305
|
concurrent-ruby version: 1.3.5
|
|
306
|
-
atomic-ruby version: 0.
|
|
306
|
+
atomic-ruby version: 0.10.0
|
|
307
307
|
|
|
308
308
|
Warming up --------------------------------------
|
|
309
309
|
Synchronized Boolean Toggle
|
|
310
|
-
|
|
310
|
+
120.000 i/100ms
|
|
311
311
|
Concurrent Ruby Atomic Boolean Toggle
|
|
312
|
-
|
|
312
|
+
94.000 i/100ms
|
|
313
313
|
Atomic Ruby Atomic Boolean Toggle
|
|
314
|
-
|
|
314
|
+
100.000 i/100ms
|
|
315
315
|
Calculating -------------------------------------
|
|
316
316
|
Synchronized Boolean Toggle
|
|
317
|
-
1.
|
|
317
|
+
1.188k (± 8.9%) i/s (841.70 μs/i) - 5.880k in 5.002927s
|
|
318
318
|
Concurrent Ruby Atomic Boolean Toggle
|
|
319
|
-
|
|
319
|
+
889.224 (±11.8%) i/s (1.12 ms/i) - 4.418k in 5.073535s
|
|
320
320
|
Atomic Ruby Atomic Boolean Toggle
|
|
321
|
-
|
|
321
|
+
999.426 (± 4.3%) i/s (1.00 ms/i) - 5.000k in 5.012997s
|
|
322
322
|
|
|
323
323
|
Comparison:
|
|
324
|
-
Synchronized Boolean Toggle:
|
|
325
|
-
Atomic Ruby Atomic Boolean Toggle:
|
|
326
|
-
Concurrent Ruby Atomic Boolean Toggle:
|
|
324
|
+
Synchronized Boolean Toggle: 1188.1 i/s
|
|
325
|
+
Atomic Ruby Atomic Boolean Toggle: 999.4 i/s - 1.19x slower
|
|
326
|
+
Concurrent Ruby Atomic Boolean Toggle: 889.2 i/s - 1.34x slower
|
|
327
327
|
```
|
|
328
328
|
|
|
329
329
|
</details>
|
|
@@ -379,13 +379,13 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
|
|
|
379
379
|
```
|
|
380
380
|
> bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
|
|
381
381
|
|
|
382
|
-
ruby version: ruby
|
|
382
|
+
ruby version: ruby 4.0.0dev (2025-11-08T15:08:09Z master 75d25a42e6) +YJIT +PRISM [arm64-darwin25]
|
|
383
383
|
concurrent-ruby version: 1.3.5
|
|
384
|
-
atomic-ruby version: 0.
|
|
384
|
+
atomic-ruby version: 0.10.0
|
|
385
385
|
|
|
386
386
|
Benchmark Results:
|
|
387
|
-
Concurrent Ruby Thread Pool: 5.
|
|
388
|
-
Atomic Ruby Atomic Thread Pool:
|
|
387
|
+
Concurrent Ruby Thread Pool: 5.56943 seconds
|
|
388
|
+
Atomic Ruby Atomic Thread Pool: 5.252876 seconds
|
|
389
389
|
```
|
|
390
390
|
|
|
391
391
|
</details>
|
|
@@ -131,12 +131,12 @@ RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
|
|
|
131
131
|
VALUE rb_cAtom = rb_define_class_under(rb_mAtomicRuby, "Atom", rb_cObject);
|
|
132
132
|
|
|
133
133
|
rb_define_alloc_func(rb_cAtom, rb_cAtom_allocate);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
rb_define_private_method(rb_cAtom, "_initialize", rb_cAtom_initialize, 1);
|
|
135
|
+
rb_define_private_method(rb_cAtom, "_value", rb_cAtom_value, 0);
|
|
136
|
+
rb_define_private_method(rb_cAtom, "_swap", rb_cAtom_swap, 0);
|
|
137
137
|
|
|
138
138
|
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
139
|
-
|
|
139
|
+
rb_define_private_method(rb_cAtom, "_initialized_ractor", rb_cAtom_initialized_ractor, 0);
|
|
140
140
|
rb_define_const(rb_mAtomicRuby, "RACTOR_SAFE", Qtrue);
|
|
141
141
|
#else
|
|
142
142
|
rb_define_const(rb_mAtomicRuby, "RACTOR_SAFE", Qfalse);
|
data/lib/atomic-ruby/atom.rb
CHANGED
|
@@ -1,17 +1,94 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require "atomic_ruby/atomic_ruby"
|
|
4
5
|
|
|
5
6
|
module AtomicRuby
|
|
7
|
+
# Provides atomic reference semantics using Compare-And-Swap (CAS) operations.
|
|
8
|
+
#
|
|
9
|
+
# An Atom allows for lock-free, thread-safe updates to a single reference value.
|
|
10
|
+
# The core operation is {#swap}, which atomically updates the value based on
|
|
11
|
+
# the current value, retrying if another thread modifies it concurrently.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# atom = Atom.new(0)
|
|
15
|
+
# atom.swap { |current_value| current_value + 1 }
|
|
16
|
+
# puts atom.value #=> 1
|
|
17
|
+
#
|
|
18
|
+
# @example Thread-safe counter
|
|
19
|
+
# counter = Atom.new(0)
|
|
20
|
+
# threads = 10.times.map do
|
|
21
|
+
# Thread.new { 100.times { counter.swap { |current_count| current_count + 1 } } }
|
|
22
|
+
# end
|
|
23
|
+
# threads.each(&:join)
|
|
24
|
+
# puts counter.value #=> 1000
|
|
25
|
+
#
|
|
26
|
+
# @example Non-mutating array operations
|
|
27
|
+
# atom = Atom.new([])
|
|
28
|
+
# atom.swap { |current_array| current_array + [1] }
|
|
29
|
+
# atom.swap { |current_array| current_array + [2] }
|
|
30
|
+
# puts atom.value #=> [1, 2]
|
|
31
|
+
#
|
|
32
|
+
# @note This class is Ractor-safe in Ruby 4.0+ when compiled with ractor support.
|
|
33
|
+
# Values that cross ractor boundaries are automatically made shareable.
|
|
6
34
|
class Atom
|
|
35
|
+
# Creates a new atomic reference with the given initial value.
|
|
36
|
+
#
|
|
37
|
+
# @param value [untyped] The initial value to store atomically
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# atom = Atom.new(42)
|
|
41
|
+
# atom = Atom.new([1, 2, 3])
|
|
42
|
+
# atom = Atom.new({ key: "value" })
|
|
43
|
+
#
|
|
44
|
+
# @rbs (untyped value) -> void
|
|
7
45
|
def initialize(value)
|
|
8
46
|
_initialize(value)
|
|
9
47
|
end
|
|
10
48
|
|
|
49
|
+
# Returns the current value stored in the atom.
|
|
50
|
+
#
|
|
51
|
+
# This operation is atomic and thread-safe. The returned value reflects
|
|
52
|
+
# the state at the time of the call, but may change immediately after
|
|
53
|
+
# in concurrent environments.
|
|
54
|
+
#
|
|
55
|
+
# @return [untyped] The current atomic value
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# atom = Atom.new("hello")
|
|
59
|
+
# puts atom.value #=> "hello"
|
|
60
|
+
#
|
|
61
|
+
# @rbs () -> untyped
|
|
11
62
|
def value
|
|
12
63
|
_value
|
|
13
64
|
end
|
|
14
65
|
|
|
66
|
+
# Atomically updates the value using a compare-and-swap operation.
|
|
67
|
+
#
|
|
68
|
+
# The block receives the current value and must return the new value.
|
|
69
|
+
# If another thread modifies the atom between reading the current value
|
|
70
|
+
# and attempting to update it, the operation retries with the new current value.
|
|
71
|
+
#
|
|
72
|
+
# @yieldparam current_value [untyped] The current atomic value
|
|
73
|
+
# @yieldreturn [untyped] The new value to store atomically
|
|
74
|
+
# @return [untyped] The new value that was successfully stored
|
|
75
|
+
#
|
|
76
|
+
# @example Increment a counter
|
|
77
|
+
# atom = Atom.new(0)
|
|
78
|
+
# new_value = atom.swap { |current_value| current_value + 1 }
|
|
79
|
+
# puts new_value #=> 1
|
|
80
|
+
#
|
|
81
|
+
# @example Append to array (non-mutating)
|
|
82
|
+
# atom = Atom.new([1, 2])
|
|
83
|
+
# atom.swap { |current_array| current_array + [3] }
|
|
84
|
+
# puts atom.value #=> [1, 2, 3]
|
|
85
|
+
#
|
|
86
|
+
# @example Conditional update
|
|
87
|
+
# atom = Atom.new(10)
|
|
88
|
+
# atom.swap { |current_value| current_value > 5 ? current_value * 2 : current_value }
|
|
89
|
+
# puts atom.value #=> 20
|
|
90
|
+
#
|
|
91
|
+
# @rbs () { (untyped) -> untyped } -> untyped
|
|
15
92
|
def swap(&block)
|
|
16
93
|
_swap do |old_value|
|
|
17
94
|
make_shareable_if_needed(block.call(old_value))
|
|
@@ -20,6 +97,15 @@ module AtomicRuby
|
|
|
20
97
|
|
|
21
98
|
private
|
|
22
99
|
|
|
100
|
+
# Makes a value shareable when crossing ractor boundaries.
|
|
101
|
+
#
|
|
102
|
+
# This method ensures ractor safety by automatically making values
|
|
103
|
+
# shareable when they need to cross ractor boundaries in Ruby 4.0+.
|
|
104
|
+
#
|
|
105
|
+
# @param value [untyped] The value to potentially make shareable
|
|
106
|
+
# @return [untyped] The original value or shareable version
|
|
107
|
+
#
|
|
108
|
+
# @rbs (untyped value) -> untyped
|
|
23
109
|
def make_shareable_if_needed(value)
|
|
24
110
|
if RACTOR_SAFE &&
|
|
25
111
|
(_initialized_ractor.nil? || Ractor.current != _initialized_ractor)
|
|
@@ -1,9 +1,50 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require_relative "atom"
|
|
4
5
|
|
|
5
6
|
module AtomicRuby
|
|
7
|
+
# Provides atomic boolean semantics with thread-safe toggle operations.
|
|
8
|
+
#
|
|
9
|
+
# AtomicBoolean wraps a boolean value in an atomic reference, providing
|
|
10
|
+
# lock-free operations for common boolean manipulations like toggling,
|
|
11
|
+
# setting to true/false, and checking the current state.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# boolean = AtomicBoolean.new(false)
|
|
15
|
+
# puts boolean.false? #=> true
|
|
16
|
+
# boolean.toggle
|
|
17
|
+
# puts boolean.true? #=> true
|
|
18
|
+
#
|
|
19
|
+
# @example Thread-safe toggle
|
|
20
|
+
# boolean = AtomicBoolean.new(false)
|
|
21
|
+
# threads = 10.times.map do
|
|
22
|
+
# Thread.new { 100.times { boolean.toggle } }
|
|
23
|
+
# end
|
|
24
|
+
# threads.each(&:join)
|
|
25
|
+
# # Final state depends on whether total toggles is even or odd
|
|
26
|
+
#
|
|
27
|
+
# @example Atomic flag setting
|
|
28
|
+
# flag = AtomicBoolean.new(false)
|
|
29
|
+
# flag.make_true
|
|
30
|
+
# puts flag.value #=> true
|
|
31
|
+
#
|
|
32
|
+
# @note This class is Ractor-safe in Ruby 4.0+ when compiled with ractor support.
|
|
6
33
|
class AtomicBoolean
|
|
34
|
+
# Creates a new atomic boolean with the given initial value.
|
|
35
|
+
#
|
|
36
|
+
# @param boolean [true, false] The initial boolean value
|
|
37
|
+
# @raise [ArgumentError] if the value is not a boolean (TrueClass or FalseClass)
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# boolean = AtomicBoolean.new(true)
|
|
41
|
+
# boolean = AtomicBoolean.new(false)
|
|
42
|
+
#
|
|
43
|
+
# @example Invalid usage
|
|
44
|
+
# AtomicBoolean.new(nil) #=> raises ArgumentError
|
|
45
|
+
# AtomicBoolean.new("true") #=> raises ArgumentError
|
|
46
|
+
#
|
|
47
|
+
# @rbs (bool boolean) -> void
|
|
7
48
|
def initialize(boolean)
|
|
8
49
|
unless boolean.is_a?(TrueClass) || boolean.is_a?(FalseClass)
|
|
9
50
|
raise ArgumentError, "boolean must be a TrueClass or FalseClass"
|
|
@@ -14,26 +55,102 @@ module AtomicRuby
|
|
|
14
55
|
Ractor.make_shareable(self) if RACTOR_SAFE
|
|
15
56
|
end
|
|
16
57
|
|
|
58
|
+
# Returns the current boolean value stored in the atom.
|
|
59
|
+
#
|
|
60
|
+
# This operation is atomic and thread-safe. The returned value reflects
|
|
61
|
+
# the state at the time of the call, but may change immediately after
|
|
62
|
+
# in concurrent environments.
|
|
63
|
+
#
|
|
64
|
+
# @return [true, false] The current atomic boolean value
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# boolean = AtomicBoolean.new(true)
|
|
68
|
+
# puts boolean.value #=> true
|
|
69
|
+
#
|
|
70
|
+
# @rbs () -> bool
|
|
17
71
|
def value
|
|
18
72
|
@boolean.value
|
|
19
73
|
end
|
|
20
74
|
|
|
75
|
+
# Tests if the current value is true.
|
|
76
|
+
#
|
|
77
|
+
# @return [true, false] true if the atomic value is true, false otherwise
|
|
78
|
+
#
|
|
79
|
+
# @example
|
|
80
|
+
# boolean = AtomicBoolean.new(true)
|
|
81
|
+
# puts boolean.true? #=> true
|
|
82
|
+
# puts boolean.false? #=> false
|
|
83
|
+
#
|
|
84
|
+
# @rbs () -> bool
|
|
21
85
|
def true?
|
|
22
86
|
value == true
|
|
23
87
|
end
|
|
24
88
|
|
|
89
|
+
# Tests if the current value is false.
|
|
90
|
+
#
|
|
91
|
+
# @return [true, false] true if the atomic value is false, false otherwise
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# boolean = AtomicBoolean.new(false)
|
|
95
|
+
# puts boolean.false? #=> true
|
|
96
|
+
# puts boolean.true? #=> false
|
|
97
|
+
#
|
|
98
|
+
# @rbs () -> bool
|
|
25
99
|
def false?
|
|
26
100
|
value == false
|
|
27
101
|
end
|
|
28
102
|
|
|
103
|
+
# Atomically sets the value to true.
|
|
104
|
+
#
|
|
105
|
+
# This operation uses compare-and-swap to ensure atomicity,
|
|
106
|
+
# making it safe for concurrent access.
|
|
107
|
+
#
|
|
108
|
+
# @return [true] Always returns true (the new value)
|
|
109
|
+
#
|
|
110
|
+
# @example
|
|
111
|
+
# boolean = AtomicBoolean.new(false)
|
|
112
|
+
# boolean.make_true
|
|
113
|
+
# puts boolean.value #=> true
|
|
114
|
+
#
|
|
115
|
+
# @rbs () -> true
|
|
29
116
|
def make_true
|
|
30
117
|
@boolean.swap { true }
|
|
31
118
|
end
|
|
32
119
|
|
|
120
|
+
# Atomically sets the value to false.
|
|
121
|
+
#
|
|
122
|
+
# This operation uses compare-and-swap to ensure atomicity,
|
|
123
|
+
# making it safe for concurrent access.
|
|
124
|
+
#
|
|
125
|
+
# @return [false] Always returns false (the new value)
|
|
126
|
+
#
|
|
127
|
+
# @example
|
|
128
|
+
# boolean = AtomicBoolean.new(true)
|
|
129
|
+
# boolean.make_false
|
|
130
|
+
# puts boolean.value #=> false
|
|
131
|
+
#
|
|
132
|
+
# @rbs () -> false
|
|
33
133
|
def make_false
|
|
34
134
|
@boolean.swap { false }
|
|
35
135
|
end
|
|
36
136
|
|
|
137
|
+
# Atomically toggles the boolean value.
|
|
138
|
+
#
|
|
139
|
+
# Changes true to false and false to true using a compare-and-swap
|
|
140
|
+
# operation, making it safe for concurrent access from multiple threads.
|
|
141
|
+
#
|
|
142
|
+
# @return [true, false] The new boolean value after toggling
|
|
143
|
+
#
|
|
144
|
+
# @example
|
|
145
|
+
# boolean = AtomicBoolean.new(false)
|
|
146
|
+
# boolean.toggle #=> true
|
|
147
|
+
# boolean.toggle #=> false
|
|
148
|
+
#
|
|
149
|
+
# @example Thread-safe toggling
|
|
150
|
+
# boolean = AtomicBoolean.new(false)
|
|
151
|
+
# 10.times.map { Thread.new { boolean.toggle } }.each(&:join)
|
|
152
|
+
#
|
|
153
|
+
# @rbs () -> bool
|
|
37
154
|
def toggle
|
|
38
155
|
@boolean.swap { |current_value| !current_value }
|
|
39
156
|
end
|
|
@@ -1,12 +1,69 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require_relative "atom"
|
|
4
5
|
|
|
5
6
|
module AtomicRuby
|
|
7
|
+
# Provides a countdown synchronization primitive using atomic operations.
|
|
8
|
+
#
|
|
9
|
+
# AtomicCountDownLatch is a synchronization aid that allows one or more threads
|
|
10
|
+
# to wait until a set of operations being performed in other threads completes.
|
|
11
|
+
# The latch is initialized with a count, and threads can wait for the count
|
|
12
|
+
# to reach zero or decrement the count atomically.
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# latch = AtomicCountDownLatch.new(3)
|
|
16
|
+
#
|
|
17
|
+
# # Start 3 worker threads
|
|
18
|
+
# 3.times do |worker_id|
|
|
19
|
+
# Thread.new do
|
|
20
|
+
# puts "Worker #{worker_id} starting"
|
|
21
|
+
# sleep(rand(2))
|
|
22
|
+
# puts "Worker #{worker_id} finished"
|
|
23
|
+
# latch.count_down
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# latch.wait # Wait for all workers to complete
|
|
28
|
+
# puts "All workers finished!"
|
|
29
|
+
#
|
|
30
|
+
# @example Waiting for initialization
|
|
31
|
+
# latch = AtomicCountDownLatch.new(1)
|
|
32
|
+
#
|
|
33
|
+
# # Start initialization in background
|
|
34
|
+
# Thread.new do
|
|
35
|
+
# puts "Initializing..."
|
|
36
|
+
# sleep(2)
|
|
37
|
+
# puts "Initialization complete"
|
|
38
|
+
# latch.count_down
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# puts "Waiting for initialization..."
|
|
42
|
+
# latch.wait
|
|
43
|
+
# puts "Ready to proceed!"
|
|
44
|
+
#
|
|
45
|
+
# @note This class is Ractor-safe in Ruby 4.0+ when compiled with ractor support.
|
|
6
46
|
class AtomicCountDownLatch
|
|
7
47
|
class Error < StandardError; end
|
|
48
|
+
|
|
49
|
+
# Error raised when attempting to count down a latch that is already at zero.
|
|
8
50
|
class AlreadyCountedDownError < Error; end
|
|
9
51
|
|
|
52
|
+
# Creates a new countdown latch with the specified count.
|
|
53
|
+
#
|
|
54
|
+
# @param count [Integer] The initial count value (must be positive)
|
|
55
|
+
#
|
|
56
|
+
# @raise [ArgumentError] if count is not a positive integer
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# latch = AtomicCountDownLatch.new(5) # Wait for 5 events
|
|
60
|
+
# latch = AtomicCountDownLatch.new(1) # Simple binary latch
|
|
61
|
+
#
|
|
62
|
+
# @example Invalid usage
|
|
63
|
+
# AtomicCountDownLatch.new(0) #=> raises ArgumentError
|
|
64
|
+
# AtomicCountDownLatch.new(-1) #=> raises ArgumentError
|
|
65
|
+
#
|
|
66
|
+
# @rbs (Integer count) -> void
|
|
10
67
|
def initialize(count)
|
|
11
68
|
unless count.is_a?(Integer) && count > 0
|
|
12
69
|
raise ArgumentError, "count must be a positive Integer"
|
|
@@ -17,10 +74,48 @@ module AtomicRuby
|
|
|
17
74
|
Ractor.make_shareable(self) if RACTOR_SAFE
|
|
18
75
|
end
|
|
19
76
|
|
|
77
|
+
# Returns the current count value.
|
|
78
|
+
#
|
|
79
|
+
# This operation is atomic and thread-safe. The returned value reflects
|
|
80
|
+
# the state at the time of the call, but may change immediately after
|
|
81
|
+
# in concurrent environments.
|
|
82
|
+
#
|
|
83
|
+
# @return [Integer] The current count (0 or positive)
|
|
84
|
+
#
|
|
85
|
+
# @example
|
|
86
|
+
# latch = AtomicCountDownLatch.new(3)
|
|
87
|
+
# puts latch.count #=> 3
|
|
88
|
+
# latch.count_down
|
|
89
|
+
# puts latch.count #=> 2
|
|
90
|
+
#
|
|
91
|
+
# @rbs () -> Integer
|
|
20
92
|
def count
|
|
21
93
|
@count.value
|
|
22
94
|
end
|
|
23
95
|
|
|
96
|
+
# Atomically decrements the count by one.
|
|
97
|
+
#
|
|
98
|
+
# If the count reaches zero, any threads waiting on {#wait} will be unblocked.
|
|
99
|
+
# This operation uses compare-and-swap to ensure atomicity and prevent
|
|
100
|
+
# race conditions.
|
|
101
|
+
#
|
|
102
|
+
# @return [Integer] The new count value after decrementing
|
|
103
|
+
#
|
|
104
|
+
# @raise [AlreadyCountedDownError] if the count is already zero
|
|
105
|
+
#
|
|
106
|
+
# @example
|
|
107
|
+
# latch = AtomicCountDownLatch.new(2)
|
|
108
|
+
# latch.count_down #=> 1
|
|
109
|
+
# latch.count_down #=> 0
|
|
110
|
+
# latch.count_down #=> raises AlreadyCountedDownError
|
|
111
|
+
#
|
|
112
|
+
# @example Thread-safe countdown
|
|
113
|
+
# latch = AtomicCountDownLatch.new(10)
|
|
114
|
+
# 10.times do
|
|
115
|
+
# Thread.new { latch.count_down }
|
|
116
|
+
# end
|
|
117
|
+
#
|
|
118
|
+
# @rbs () -> Integer
|
|
24
119
|
def count_down
|
|
25
120
|
already_counted_down = false
|
|
26
121
|
new_count = @count.swap do |current_count|
|
|
@@ -36,6 +131,40 @@ module AtomicRuby
|
|
|
36
131
|
new_count
|
|
37
132
|
end
|
|
38
133
|
|
|
134
|
+
# Blocks the current thread until the count reaches zero.
|
|
135
|
+
#
|
|
136
|
+
# This method will block the calling thread until other threads have
|
|
137
|
+
# called {#count_down} enough times to reduce the count to zero.
|
|
138
|
+
# The method uses busy-waiting (Thread.pass) to check the count.
|
|
139
|
+
#
|
|
140
|
+
# @return [void]
|
|
141
|
+
#
|
|
142
|
+
# @example Simple wait
|
|
143
|
+
# latch = AtomicCountDownLatch.new(1)
|
|
144
|
+
#
|
|
145
|
+
# Thread.new do
|
|
146
|
+
# sleep(2)
|
|
147
|
+
# latch.count_down
|
|
148
|
+
# end
|
|
149
|
+
#
|
|
150
|
+
# latch.wait # Blocks for ~2 seconds
|
|
151
|
+
# puts "Latch opened!"
|
|
152
|
+
#
|
|
153
|
+
# @example Waiting for multiple events
|
|
154
|
+
# latch = AtomicCountDownLatch.new(3)
|
|
155
|
+
#
|
|
156
|
+
# 3.times do |event_id|
|
|
157
|
+
# Thread.new do
|
|
158
|
+
# sleep(1 + event_id)
|
|
159
|
+
# puts "Event #{event_id} completed"
|
|
160
|
+
# latch.count_down
|
|
161
|
+
# end
|
|
162
|
+
# end
|
|
163
|
+
#
|
|
164
|
+
# latch.wait # Waits for all 3 events
|
|
165
|
+
# puts "All events completed!"
|
|
166
|
+
#
|
|
167
|
+
# @rbs () -> void
|
|
39
168
|
def wait
|
|
40
169
|
Thread.pass while @count.value > 0
|
|
41
170
|
end
|
|
@@ -1,15 +1,68 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require_relative "atom"
|
|
4
5
|
|
|
5
6
|
module AtomicRuby
|
|
7
|
+
# Provides a fixed-size thread pool using atomic operations for work queuing.
|
|
8
|
+
#
|
|
9
|
+
# AtomicThreadPool maintains a fixed number of worker threads that process
|
|
10
|
+
# work items from an atomic queue. The pool uses compare-and-swap operations
|
|
11
|
+
# for thread-safe work enqueueing and state management.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# pool = AtomicThreadPool.new(size: 4)
|
|
15
|
+
# pool << proc { puts "Hello from worker thread!" }
|
|
16
|
+
# pool << proc { puts "Another work item" }
|
|
17
|
+
# pool.shutdown
|
|
18
|
+
#
|
|
19
|
+
# @example Processing work with results
|
|
20
|
+
# results = []
|
|
21
|
+
# pool = AtomicThreadPool.new(size: 2, name: "Calculator")
|
|
22
|
+
#
|
|
23
|
+
# 10.times do |index|
|
|
24
|
+
# pool << proc { results << index * 2 }
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# pool.shutdown
|
|
28
|
+
# puts results.sort #=> [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
|
|
29
|
+
#
|
|
30
|
+
# @example Monitoring pool state
|
|
31
|
+
# pool = AtomicThreadPool.new(size: 3)
|
|
32
|
+
# puts pool.length #=> 3
|
|
33
|
+
# puts pool.queue_length #=> 0
|
|
34
|
+
# puts pool.active_count #=> 0
|
|
35
|
+
#
|
|
36
|
+
# 5.times { pool << proc { sleep(1) } }
|
|
37
|
+
# puts pool.queue_length #=> 2 (3 workers busy, 2 queued)
|
|
38
|
+
# puts pool.active_count #=> 3 (3 workers processing)
|
|
39
|
+
#
|
|
40
|
+
# @note This class is NOT Ractor-safe as it contains mutable thread state
|
|
41
|
+
# that cannot be safely shared across ractors.
|
|
6
42
|
class AtomicThreadPool
|
|
7
43
|
class Error < StandardError; end
|
|
8
44
|
|
|
45
|
+
# Error raised when attempting to enqueue work after shutdown.
|
|
9
46
|
class EnqueuedWorkAfterShutdownError < Error
|
|
47
|
+
# @rbs () -> String
|
|
10
48
|
def message = "cannot queue work after shutdown"
|
|
11
49
|
end
|
|
12
50
|
|
|
51
|
+
# Creates a new thread pool with the specified size.
|
|
52
|
+
#
|
|
53
|
+
# @param size [Integer] The number of worker threads to create (must be positive)
|
|
54
|
+
# @param name [String, nil] Optional name for the thread pool (used in thread names)
|
|
55
|
+
#
|
|
56
|
+
# @raise [ArgumentError] if size is not a positive integer
|
|
57
|
+
# @raise [ArgumentError] if name is provided but not a string
|
|
58
|
+
#
|
|
59
|
+
# @example Create a basic pool
|
|
60
|
+
# pool = AtomicThreadPool.new(size: 4)
|
|
61
|
+
#
|
|
62
|
+
# @example Create a named pool
|
|
63
|
+
# pool = AtomicThreadPool.new(size: 2, name: "Database Workers")
|
|
64
|
+
#
|
|
65
|
+
# @rbs (size: Integer, ?name: String?) -> void
|
|
13
66
|
def initialize(size:, name: nil)
|
|
14
67
|
raise ArgumentError, "size must be a positive Integer" unless size.is_a?(Integer) && size > 0
|
|
15
68
|
raise ArgumentError, "name must be a String" unless name.nil? || name.is_a?(String)
|
|
@@ -18,12 +71,35 @@ module AtomicRuby
|
|
|
18
71
|
@name = name
|
|
19
72
|
|
|
20
73
|
@state = Atom.new(queue: [], shutdown: false)
|
|
21
|
-
@
|
|
74
|
+
@started_thread_count = Atom.new(0)
|
|
75
|
+
@active_thread_count = Atom.new(0)
|
|
22
76
|
@threads = []
|
|
23
77
|
|
|
24
78
|
start
|
|
25
79
|
end
|
|
26
80
|
|
|
81
|
+
# Enqueues work to be executed by the thread pool.
|
|
82
|
+
#
|
|
83
|
+
# The work item must respond to #call (typically a Proc or lambda).
|
|
84
|
+
# Work items are executed in FIFO order by available worker threads.
|
|
85
|
+
# If all workers are busy, the work is queued atomically.
|
|
86
|
+
#
|
|
87
|
+
# @param work [#call] A callable object to be executed by a worker thread
|
|
88
|
+
#
|
|
89
|
+
# @raise [EnqueuedWorkAfterShutdownError] if the pool has been shut down
|
|
90
|
+
#
|
|
91
|
+
# @example Enqueue a simple task
|
|
92
|
+
# pool << proc { puts "Hello World" }
|
|
93
|
+
#
|
|
94
|
+
# @example Enqueue a lambda with parameters
|
|
95
|
+
# calculator = ->(a, b) { puts a + b }
|
|
96
|
+
# pool << proc { calculator.call(2, 3) }
|
|
97
|
+
#
|
|
98
|
+
# @example Enqueue work that captures variables
|
|
99
|
+
# name = "Alice"
|
|
100
|
+
# pool << proc { puts "Processing #{name}" }
|
|
101
|
+
#
|
|
102
|
+
# @rbs (Proc work) -> void
|
|
27
103
|
def <<(work)
|
|
28
104
|
state = @state.swap do |current_state|
|
|
29
105
|
if current_state[:shutdown]
|
|
@@ -35,16 +111,98 @@ module AtomicRuby
|
|
|
35
111
|
raise EnqueuedWorkAfterShutdownError if state[:shutdown]
|
|
36
112
|
end
|
|
37
113
|
|
|
114
|
+
# Returns the number of currently alive worker threads.
|
|
115
|
+
#
|
|
116
|
+
# This count decreases as the pool shuts down and threads terminate.
|
|
117
|
+
# During normal operation, this should equal the size parameter
|
|
118
|
+
# passed to the constructor.
|
|
119
|
+
#
|
|
120
|
+
# @return [Integer] The number of alive worker threads
|
|
121
|
+
#
|
|
122
|
+
# @example
|
|
123
|
+
# pool = AtomicThreadPool.new(size: 4)
|
|
124
|
+
# puts pool.length #=> 4
|
|
125
|
+
# pool.shutdown
|
|
126
|
+
# puts pool.length #=> 0
|
|
127
|
+
#
|
|
128
|
+
# @rbs () -> Integer
|
|
38
129
|
def length
|
|
39
130
|
@threads.select(&:alive?).length
|
|
40
131
|
end
|
|
132
|
+
# Alias for {#length}.
|
|
133
|
+
# @rbs () -> Integer
|
|
41
134
|
alias size length
|
|
42
135
|
|
|
136
|
+
# Returns the number of work items currently queued for execution.
|
|
137
|
+
#
|
|
138
|
+
# This represents work that has been enqueued but not yet picked up
|
|
139
|
+
# by a worker thread. A high queue length indicates that work is
|
|
140
|
+
# being submitted faster than it can be processed.
|
|
141
|
+
#
|
|
142
|
+
# @return [Integer] The number of queued work items
|
|
143
|
+
#
|
|
144
|
+
# @example
|
|
145
|
+
# pool = AtomicThreadPool.new(size: 2)
|
|
146
|
+
# 5.times { pool << proc { sleep(1) } }
|
|
147
|
+
# puts pool.queue_length #=> 3 (2 workers busy, 3 queued)
|
|
148
|
+
#
|
|
149
|
+
# @rbs () -> Integer
|
|
43
150
|
def queue_length
|
|
44
151
|
@state.value[:queue].length
|
|
45
152
|
end
|
|
153
|
+
# Alias for {#queue_length}.
|
|
154
|
+
# @rbs () -> Integer
|
|
46
155
|
alias queue_size queue_length
|
|
47
156
|
|
|
157
|
+
# Returns the number of worker threads currently executing work.
|
|
158
|
+
#
|
|
159
|
+
# This represents threads that have picked up a work item and are
|
|
160
|
+
# actively processing it. The count includes threads in the middle
|
|
161
|
+
# of executing work.call, but excludes threads that are idle or
|
|
162
|
+
# waiting for work.
|
|
163
|
+
#
|
|
164
|
+
# @return [Integer] The number of threads actively processing work
|
|
165
|
+
#
|
|
166
|
+
# @example Monitor active workers
|
|
167
|
+
# pool = AtomicThreadPool.new(size: 4)
|
|
168
|
+
# puts pool.active_count #=> 0
|
|
169
|
+
#
|
|
170
|
+
# 5.times { pool << proc { sleep(1) } }
|
|
171
|
+
# sleep(0.1) # Give threads time to pick up work
|
|
172
|
+
# puts pool.active_count #=> 4 (all workers busy)
|
|
173
|
+
# puts pool.queue_length #=> 1 (one item still queued)
|
|
174
|
+
#
|
|
175
|
+
# @example Calculate total load
|
|
176
|
+
# total_load = pool.active_count + pool.queue_length
|
|
177
|
+
# puts "Total pending work: #{total_load}"
|
|
178
|
+
#
|
|
179
|
+
# @rbs () -> Integer
|
|
180
|
+
def active_count
|
|
181
|
+
@active_thread_count.value
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Gracefully shuts down the thread pool.
|
|
185
|
+
#
|
|
186
|
+
# This method:
|
|
187
|
+
# 1. Marks the pool as shutdown (preventing new work from being enqueued)
|
|
188
|
+
# 2. Waits for all currently queued work to complete
|
|
189
|
+
# 3. Waits for all worker threads to terminate
|
|
190
|
+
#
|
|
191
|
+
# After shutdown, all worker threads will be terminated and the pool
|
|
192
|
+
# cannot be restarted. Attempting to enqueue work after shutdown
|
|
193
|
+
# will raise an exception.
|
|
194
|
+
#
|
|
195
|
+
# @return [void]
|
|
196
|
+
#
|
|
197
|
+
# @raise [EnqueuedWorkAfterShutdownError] if work is enqueued after shutdown
|
|
198
|
+
#
|
|
199
|
+
# @example
|
|
200
|
+
# pool = AtomicThreadPool.new(size: 4)
|
|
201
|
+
# 10.times { |index| pool << proc { puts index } }
|
|
202
|
+
# pool.shutdown # waits for all work to complete
|
|
203
|
+
# puts pool.length #=> 0
|
|
204
|
+
#
|
|
205
|
+
# @rbs () -> void
|
|
48
206
|
def shutdown
|
|
49
207
|
already_shutdown = false
|
|
50
208
|
@state.swap do |current_state|
|
|
@@ -64,6 +222,14 @@ module AtomicRuby
|
|
|
64
222
|
|
|
65
223
|
private
|
|
66
224
|
|
|
225
|
+
# Starts the worker threads for the thread pool.
|
|
226
|
+
#
|
|
227
|
+
# This method is called automatically during initialization.
|
|
228
|
+
# It creates the specified number of worker threads and waits
|
|
229
|
+
# for all threads to be fully started before returning.
|
|
230
|
+
#
|
|
231
|
+
# @return [void]
|
|
232
|
+
# @rbs () -> void
|
|
67
233
|
def start
|
|
68
234
|
@size.times do |num|
|
|
69
235
|
@threads << Thread.new(num) do |idx|
|
|
@@ -71,7 +237,7 @@ module AtomicRuby
|
|
|
71
237
|
thread_name << " for #{@name}" if @name
|
|
72
238
|
Thread.current.name = thread_name
|
|
73
239
|
|
|
74
|
-
@
|
|
240
|
+
@started_thread_count.swap { |current_count| current_count + 1 }
|
|
75
241
|
|
|
76
242
|
loop do
|
|
77
243
|
work = nil
|
|
@@ -92,12 +258,15 @@ module AtomicRuby
|
|
|
92
258
|
if should_shutdown
|
|
93
259
|
break
|
|
94
260
|
elsif work
|
|
261
|
+
@active_thread_count.swap { |current_count| current_count + 1 }
|
|
95
262
|
begin
|
|
96
263
|
work.call
|
|
97
264
|
rescue => err
|
|
98
265
|
puts "#{thread_name} rescued:"
|
|
99
266
|
puts "#{err.class}: #{err.message}"
|
|
100
267
|
puts err.backtrace.join("\n")
|
|
268
|
+
ensure
|
|
269
|
+
@active_thread_count.swap { |current_count| current_count - 1 }
|
|
101
270
|
end
|
|
102
271
|
else
|
|
103
272
|
Thread.pass
|
|
@@ -107,7 +276,7 @@ module AtomicRuby
|
|
|
107
276
|
end
|
|
108
277
|
@threads.freeze
|
|
109
278
|
|
|
110
|
-
Thread.pass until @
|
|
279
|
+
Thread.pass until @started_thread_count.value == @size
|
|
111
280
|
end
|
|
112
281
|
end
|
|
113
282
|
end
|
data/lib/atomic-ruby/version.rb
CHANGED