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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bef30ee19f89bf213e837f5ee565b4e0b2f926c422dd7204270f349a331142c
4
- data.tar.gz: b19ac4ac6c8f363c891aae5c7ef4b4c39c80b420673f71890fe8c4f1974dee89
3
+ metadata.gz: b52931f98d46947d0d2f4dccffbe51e771c619d61b54457a223162be41624353
4
+ data.tar.gz: 3c91a27d99c3eb4d2a04f668bc5ebbc19c1c60b6a9fc688812df96c20bed37a4
5
5
  SHA512:
6
- metadata.gz: 8afd84e8e6cc724949c88c6485bd51e8d3ba86e56852ad742136613ce2f4a36468de88b7c011a2061218ff7edb189a8e8a13f3049741d5a020862c8cc9760e7d
7
- data.tar.gz: d5727d75e5b3c6f4cce6c7dd394b1fd7d64d48f3940124a27f2725e1947c6aac375604fb98364a24aa6bbcb50670680b3b755c566f5092829b21eff7bef98d10
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 (requires Ruby 3.5+)
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 3.5+.
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 3.5.0dev (2025-11-05T10:35:48Z master 946d2d036f) +YJIT +PRISM [arm64-darwin25]
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.9.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.104234 seconds
224
- Concurrent Ruby Atomic Bank Account: 5.113334 seconds
225
- Atomic Ruby Atomic Bank Account: 5.097197 seconds
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 3.5.0dev (2025-11-05T10:35:48Z master 946d2d036f) +YJIT +PRISM [arm64-darwin25]
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.9.0
306
+ atomic-ruby version: 0.10.0
307
307
 
308
308
  Warming up --------------------------------------
309
309
  Synchronized Boolean Toggle
310
- 158.000 i/100ms
310
+ 120.000 i/100ms
311
311
  Concurrent Ruby Atomic Boolean Toggle
312
- 113.000 i/100ms
312
+ 94.000 i/100ms
313
313
  Atomic Ruby Atomic Boolean Toggle
314
- 122.000 i/100ms
314
+ 100.000 i/100ms
315
315
  Calculating -------------------------------------
316
316
  Synchronized Boolean Toggle
317
- 1.521k2.1%) i/s (657.49 μs/i) - 7.742k in 5.092579s
317
+ 1.188k8.9%) i/s (841.70 μs/i) - 5.880k in 5.002927s
318
318
  Concurrent Ruby Atomic Boolean Toggle
319
- 1.141k 1.6%) i/s (876.12 μs/i) - 5.763k in 5.050298s
319
+ 889.22411.8%) i/s (1.12 ms/i) - 4.418k in 5.073535s
320
320
  Atomic Ruby Atomic Boolean Toggle
321
- 1.243k1.3%) i/s (804.64 μs/i) - 6.222k in 5.007246s
321
+ 999.4264.3%) i/s (1.00 ms/i) - 5.000k in 5.012997s
322
322
 
323
323
  Comparison:
324
- Synchronized Boolean Toggle: 1520.9 i/s
325
- Atomic Ruby Atomic Boolean Toggle: 1242.8 i/s - 1.22x slower
326
- Concurrent Ruby Atomic Boolean Toggle: 1141.4 i/s - 1.33x slower
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 3.5.0dev (2025-11-05T10:35:48Z master 946d2d036f) +YJIT +PRISM [arm64-darwin25]
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.9.0
384
+ atomic-ruby version: 0.10.0
385
385
 
386
386
  Benchmark Results:
387
- Concurrent Ruby Thread Pool: 5.169928 seconds
388
- Atomic Ruby Atomic Thread Pool: 4.831942 seconds
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
- rb_define_method(rb_cAtom, "_initialize", rb_cAtom_initialize, 1);
135
- rb_define_method(rb_cAtom, "_value", rb_cAtom_value, 0);
136
- rb_define_method(rb_cAtom, "_swap", rb_cAtom_swap, 0);
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
- rb_define_method(rb_cAtom, "_initialized_ractor", rb_cAtom_initialized_ractor, 0);
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);
@@ -5,7 +5,7 @@
5
5
  #include "ruby/atomic.h"
6
6
  #include "ruby/version.h"
7
7
 
8
- #if RUBY_API_VERSION_CODE >= 30500
8
+ #if RUBY_API_VERSION_CODE >= 40000
9
9
  #define ATOMIC_RUBY_RACTOR_SAFE 1
10
10
  #include "ruby/ractor.h"
11
11
  #endif
@@ -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
- @started_threads = Atom.new(0)
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
- @started_threads.swap { |current_count| current_count + 1 }
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 @started_threads.value == @size
279
+ Thread.pass until @started_thread_count.value == @size
111
280
  end
112
281
  end
113
282
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomic-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young