atomic-ruby 0.15.2 → 0.15.3

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: 9c6210195ad6d82fbbb6a84f66bc8ce5faef54d50ed3227c3655ec43a996a9f6
4
- data.tar.gz: eef94abbb8c8bfb1d9a54d36f8f8b0404d49210e6e6a109851fa014a6512aaa5
3
+ metadata.gz: a50be9a8594152f8d07b2338588a01be6aecadf55f993ea6cc4c2431e80e2b76
4
+ data.tar.gz: eb9c45d8dff70d0697d3d62ba5a539e3d71e42c21ea7ab6b646d44fde69f39ac
5
5
  SHA512:
6
- metadata.gz: c89b7c62bda07206115c80c45088d1ae44e5c3aa84288932fae8fad51312d3490bf9510af559178fe6311e5d8ce4ecf87dca0da3420ced47a0113783e63a741d
7
- data.tar.gz: a5c5ecee1ce3f96b5e2c525897c1c979fa3f99b7be6cd971d6882e2a43bf9b735f0de58bcf67dbf1179b0298a006a25ba45e910ee0c104f965c305e4af93f6be
6
+ metadata.gz: abde2bc85010be3634caeaf66d262acf1c5c70c50af5e9e420a43c87071965bd543a475b01b5d1b0d1569682802595e936a9d224e636896a681e5c3c85d98a77
7
+ data.tar.gz: e93d7b35c3ccdba9c82906587c24455c248655ffbe3f9d4141e4cb029705306f80c13b36f6a0ce7193a33701758ad0a9c2f77a7aa0c94ad92e49392ba899c055
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.15.2] - 2026-08-31
4
+
5
+ - Prevent enqueues from racing `AtomicThreadPool#shutdown`
6
+ - Remove interrupted waiters from `AtomicConditionVariable`
7
+ - Replace `Thread.pass` busy-wait with `sleep` in `AtomicCountDownLatch#wait`
8
+
3
9
  ## [0.15.2] - 2026-08-30
4
10
 
5
11
  - Avoid thread event hook overhead for fixed thread pools
data/README.md CHANGED
@@ -267,8 +267,8 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
267
267
  > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
268
268
 
269
269
  ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
270
- concurrent-ruby version: 1.3.6
271
- atomic-ruby version: 0.15.2
270
+ concurrent-ruby version: 1.3.8
271
+ atomic-ruby version: 0.15.3
272
272
 
273
273
  Balances:
274
274
  Synchronized Bank Account Balance: 975
@@ -276,9 +276,9 @@ Concurrent Ruby Atomic Bank Account Balance: 975
276
276
  Atomic Ruby Atomic Bank Account Balance: 975
277
277
 
278
278
  Benchmark Results:
279
- Synchronized Bank Account: 5.117072 seconds
280
- Concurrent Ruby Atomic Bank Account: 5.128366 seconds
281
- Atomic Ruby Atomic Bank Account: 5.110595 seconds
279
+ Synchronized Bank Account: 5.104959 seconds
280
+ Concurrent Ruby Atomic Bank Account: 5.120305 seconds
281
+ Atomic Ruby Atomic Bank Account: 5.102732 seconds
282
282
  ```
283
283
 
284
284
  </details>
@@ -358,22 +358,22 @@ end
358
358
  > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
359
359
 
360
360
  ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
361
- concurrent-ruby version: 1.3.6
362
- atomic-ruby version: 0.15.2
361
+ concurrent-ruby version: 1.3.8
362
+ atomic-ruby version: 0.15.3
363
363
 
364
364
  Warming up --------------------------------------
365
- Synchronized Boolean Toggle 157.000 i/100ms
366
- Concurrent Ruby Atomic Boolean Toggle 134.000 i/100ms
367
- Atomic Ruby Atomic Boolean Toggle 120.000 i/100ms
365
+ Synchronized Boolean Toggle 165.000 i/100ms
366
+ Concurrent Ruby Atomic Boolean Toggle 132.000 i/100ms
367
+ Atomic Ruby Atomic Boolean Toggle 119.000 i/100ms
368
368
  Calculating -------------------------------------
369
- Synchronized Boolean Toggle 1.629k (± 2.0%) i/s (613.87 μs/i) - 8.164k in 5.011636s
370
- Concurrent Ruby Atomic Boolean Toggle 1.325k2.0%) i/s (754.96 μs/i) - 6.700k in 5.058203s
371
- Atomic Ruby Atomic Boolean Toggle 1.190k2.6%) i/s (840.11 μs/i) - 6.000k in 5.040665s
369
+ Synchronized Boolean Toggle 1.647k (± 2.1%) i/s (606.98 μs/i) - 8.250k in 5.007598s
370
+ Concurrent Ruby Atomic Boolean Toggle 1.343k1.9%) i/s (744.55 μs/i) - 6.732k in 5.012307s
371
+ Atomic Ruby Atomic Boolean Toggle 1.207k1.9%) i/s (828.31 μs/i) - 6.069k in 5.027033s
372
372
 
373
373
  Comparison:
374
- Synchronized Boolean Toggle: 1629.0 i/s
375
- Concurrent Ruby Atomic Boolean Toggle: 1324.6 i/s - 1.23x slower
376
- Atomic Ruby Atomic Boolean Toggle: 1190.3 i/s - 1.37x slower
374
+ Synchronized Boolean Toggle: 1647.5 i/s
375
+ Concurrent Ruby Atomic Boolean Toggle: 1343.1 i/s - 1.23x slower
376
+ Atomic Ruby Atomic Boolean Toggle: 1207.3 i/s - 1.36x slower
377
377
  ```
378
378
 
379
379
  </details>
@@ -442,18 +442,18 @@ end
442
442
  > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_condition_variable_benchmark.rb
443
443
 
444
444
  ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
445
- atomic-ruby version: 0.15.2
445
+ atomic-ruby version: 0.15.3
446
446
 
447
447
  Warming up --------------------------------------
448
- Synchronized Condition Variable Wait/Signal 3.977k i/100ms
449
- Atomic Ruby Atomic Condition Variable Wait/Signal 3.885k i/100ms
448
+ Synchronized Condition Variable Wait/Signal 4.062k i/100ms
449
+ Atomic Ruby Atomic Condition Variable Wait/Signal 4.133k i/100ms
450
450
  Calculating -------------------------------------
451
- Synchronized Condition Variable Wait/Signal 39.458k4.2%) i/s (25.34 μs/i) - 198.850k in 5.039596s
452
- Atomic Ruby Atomic Condition Variable Wait/Signal 39.046k4.2%) i/s (25.61 μs/i) - 198.135k in 5.074411s
451
+ Synchronized Condition Variable Wait/Signal 41.923k3.5%) i/s (23.85 μs/i) - 211.224k in 5.038330s
452
+ Atomic Ruby Atomic Condition Variable Wait/Signal 40.666k3.8%) i/s (24.59 μs/i) - 206.650k in 5.081595s
453
453
 
454
454
  Comparison:
455
- Synchronized Condition Variable Wait/Signal: 39457.5 i/s
456
- Atomic Ruby Atomic Condition Variable Wait/Signal: 39045.9 i/s - same-ish: difference falls within error
455
+ Synchronized Condition Variable Wait/Signal: 41923.4 i/s
456
+ Atomic Ruby Atomic Condition Variable Wait/Signal: 40666.4 i/s - same-ish: difference falls within error
457
457
  ```
458
458
 
459
459
  </details>
@@ -517,18 +517,18 @@ end
517
517
  > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_queue_benchmark.rb
518
518
 
519
519
  ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
520
- atomic-ruby version: 0.15.2
520
+ atomic-ruby version: 0.15.3
521
521
 
522
522
  Warming up --------------------------------------
523
- Synchronized Queue Push/Pop 181.000 i/100ms
524
- Atomic Ruby Atomic Queue Push/Pop 132.000 i/100ms
523
+ Synchronized Queue Push/Pop 184.000 i/100ms
524
+ Atomic Ruby Atomic Queue Push/Pop 149.000 i/100ms
525
525
  Calculating -------------------------------------
526
- Synchronized Queue Push/Pop 1.845k (± 2.1%) i/s (541.91 μs/i) - 9.231k in 5.002394s
527
- Atomic Ruby Atomic Queue Push/Pop 1.431k6.8%) i/s (698.64 μs/i) - 7.260k in 5.072143s
526
+ Synchronized Queue Push/Pop 1.819k (± 2.0%) i/s (549.73 μs/i) - 9.200k in 5.057550s
527
+ Atomic Ruby Atomic Queue Push/Pop 1.493k1.9%) i/s (669.87 μs/i) - 7.599k in 5.090319s
528
528
 
529
529
  Comparison:
530
- Synchronized Queue Push/Pop: 1845.3 i/s
531
- Atomic Ruby Atomic Queue Push/Pop: 1431.3 i/s - 1.29x slower
530
+ Synchronized Queue Push/Pop: 1819.1 i/s
531
+ Atomic Ruby Atomic Queue Push/Pop: 1492.8 i/s - 1.22x slower
532
532
 
533
533
  ```
534
534
 
@@ -587,11 +587,69 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
587
587
 
588
588
  ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
589
589
  concurrent-ruby version: 1.3.8
590
- atomic-ruby version: 0.15.2
590
+ atomic-ruby version: 0.15.3
591
+
592
+ Benchmark Results:
593
+ Concurrent Ruby Thread Pool: 5.012798 seconds
594
+ Atomic Ruby Atomic Thread Pool: 4.755237 seconds
595
+ ```
596
+
597
+ </details>
598
+
599
+ <details>
600
+
601
+ <summary>AtomicCountDownLatch</summary>
602
+
603
+ <br>
604
+
605
+ ```ruby
606
+ # frozen_string_literal: true
607
+
608
+ require "benchmark"
609
+ require "concurrent-ruby"
610
+ require_relative "../lib/atomic-ruby"
611
+
612
+ results = []
613
+
614
+ 2.times do |idx|
615
+ result = Benchmark.measure do
616
+ latch = case idx
617
+ when 0 then Concurrent::CountDownLatch.new(1)
618
+ when 1 then AtomicCountDownLatch.new(1)
619
+ end
620
+
621
+ thread = Thread.new do
622
+ sleep(2)
623
+ latch.count_down
624
+ end
625
+
626
+ latch.wait
627
+ thread.join
628
+ end
629
+
630
+ results << result
631
+ end
632
+
633
+ puts "\n"
634
+ puts "ruby version: #{RUBY_DESCRIPTION}"
635
+ puts "concurrent-ruby version: #{Concurrent::VERSION}"
636
+ puts "atomic-ruby version: #{AtomicRuby::VERSION}"
637
+ puts "\n"
638
+ puts "Benchmark Results:"
639
+ puts "Concurrent Ruby Count Down Latch: #{results[0].total.round(6)} CPU seconds, #{results[0].real.round(6)} elapsed seconds"
640
+ puts "Atomic Ruby Atomic Count Down Latch: #{results[1].total.round(6)} CPU seconds, #{results[1].real.round(6)} elapsed seconds"
641
+ ```
642
+
643
+ ```
644
+ > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_count_down_latch_benchmark.rb
645
+
646
+ ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
647
+ concurrent-ruby version: 1.3.8
648
+ atomic-ruby version: 0.15.3
591
649
 
592
650
  Benchmark Results:
593
- Concurrent Ruby Thread Pool: 5.081233 seconds
594
- Atomic Ruby Atomic Thread Pool: 4.737824 seconds
651
+ Concurrent Ruby Count Down Latch: 0.000511 CPU seconds, 2.001465 elapsed seconds
652
+ Atomic Ruby Atomic Count Down Latch: 0.023529 CPU seconds, 2.00364 elapsed seconds
595
653
  ```
596
654
 
597
655
  </details>
@@ -172,6 +172,7 @@ typedef struct {
172
172
  volatile VALUE head;
173
173
  volatile VALUE tail;
174
174
  volatile rb_atomic_t count;
175
+ bool closed;
175
176
  } atomic_ruby_queue_t;
176
177
 
177
178
  static void atomic_ruby_queue_mark(void *ptr) {
@@ -211,6 +212,7 @@ static VALUE rb_cAtomicQueue_allocate(VALUE klass) {
211
212
  RB_OBJ_WRITE(obj, &atomic_ruby_queue->head, Qnil);
212
213
  RB_OBJ_WRITE(obj, &atomic_ruby_queue->tail, Qnil);
213
214
  atomic_ruby_queue->count = 0;
215
+ atomic_ruby_queue->closed = false;
214
216
  return obj;
215
217
  }
216
218
 
@@ -228,6 +230,7 @@ static VALUE rb_cAtomicQueue_initialize(VALUE self) {
228
230
  static VALUE rb_cAtomicQueue_push(VALUE self, VALUE value) {
229
231
  atomic_ruby_queue_t *atomic_ruby_queue;
230
232
  TypedData_Get_Struct(self, atomic_ruby_queue_t, &atomic_ruby_queue_type, atomic_ruby_queue);
233
+ if (atomic_ruby_queue->closed) return Qfalse;
231
234
 
232
235
  VALUE new_node = atomic_ruby_queue_node_new(value, Qnil);
233
236
 
@@ -259,6 +262,13 @@ static VALUE rb_cAtomicQueue_push(VALUE self, VALUE value) {
259
262
  return self;
260
263
  }
261
264
 
265
+ static VALUE rb_cAtomicQueue_close(VALUE self) {
266
+ atomic_ruby_queue_t *atomic_ruby_queue;
267
+ TypedData_Get_Struct(self, atomic_ruby_queue_t, &atomic_ruby_queue_type, atomic_ruby_queue);
268
+ atomic_ruby_queue->closed = true;
269
+ return Qnil;
270
+ }
271
+
262
272
  static VALUE rb_cAtomicQueue_pop(VALUE self) {
263
273
  atomic_ruby_queue_t *atomic_ruby_queue;
264
274
  TypedData_Get_Struct(self, atomic_ruby_queue_t, &atomic_ruby_queue_type, atomic_ruby_queue);
@@ -787,6 +797,7 @@ RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
787
797
  rb_define_alloc_func(rb_cAtomicQueue, rb_cAtomicQueue_allocate);
788
798
  rb_define_private_method(rb_cAtomicQueue, "_initialize", rb_cAtomicQueue_initialize, 0);
789
799
  rb_define_private_method(rb_cAtomicQueue, "_push", rb_cAtomicQueue_push, 1);
800
+ rb_define_private_method(rb_cAtomicQueue, "_close", rb_cAtomicQueue_close, 0);
790
801
  rb_define_private_method(rb_cAtomicQueue, "_pop", rb_cAtomicQueue_pop, 0);
791
802
  rb_define_private_method(rb_cAtomicQueue, "_size", rb_cAtomicQueue_size, 0);
792
803
  rb_define_private_method(rb_cAtomicQueue, "_empty_p", rb_cAtomicQueue_empty_p, 0);
@@ -8,19 +8,10 @@ module AtomicRuby
8
8
  #
9
9
  # AtomicConditionVariable lets one or more threads park until another
10
10
  # thread signals them, without the paired `Mutex` that Ruby's
11
- # `ConditionVariable` requires. The set of parked threads is tracked
12
- # in a native doubly-linked list, so registering a waiter, removing a
13
- # waiter, and shifting the head off for a signal are all O(1) and
14
- # allocation-light. Parking still uses `Thread.stop` and
15
- # `Thread#wakeup`, which are the standard kernel-level primitives Ruby
16
- # exposes for sleeping a thread.
11
+ # `ConditionVariable` requires.
17
12
  #
18
- # The lost-wakeup race in a naive `check-then-park` consumer is avoided
19
- # by the {#wait} contract: a waiter registers itself before re-evaluating
20
- # the predicate, so any signal that fires after the producer makes the
21
- # predicate true is guaranteed to see the waiter and wake it. Ruby also
22
- # remembers pending wakeups across `Thread.stop`, so a wakeup that
23
- # arrives between the predicate check and the actual park is not lost.
13
+ # Waiters supply a predicate that is checked again after registration,
14
+ # preventing signals sent after a relevant state change from being lost.
24
15
  #
25
16
  # @example Basic usage
26
17
  # condvar = AtomicConditionVariable.new
@@ -72,10 +63,6 @@ module AtomicRuby
72
63
 
73
64
  # Wakes one parked waiter, or no-ops if none are parked.
74
65
  #
75
- # If a waiter has registered itself but is not yet inside `Thread.stop`,
76
- # Ruby remembers the wakeup and the next `Thread.stop` returns
77
- # immediately.
78
- #
79
66
  # @return [true, false] true if a waiter was signalled, false otherwise
80
67
  #
81
68
  # @example
@@ -93,8 +80,6 @@ module AtomicRuby
93
80
 
94
81
  # Wakes every parked waiter.
95
82
  #
96
- # Each woken thread observes the wake the same way as with {#signal}.
97
- #
98
83
  # @return [Integer] The number of waiters signalled
99
84
  #
100
85
  # @example
@@ -111,12 +96,10 @@ module AtomicRuby
111
96
  # Blocks until the given block returns a truthy value, then returns that
112
97
  # value.
113
98
  #
114
- # The block is evaluated optimistically first. If it returns truthy on
115
- # that pass, no waiter is registered and the call returns immediately.
116
- # Otherwise the calling thread registers itself, re-evaluates the
117
- # block, and parks via `Thread.stop` until a {#signal} or {#broadcast}
118
- # wakes it. The block may run more than once and may run concurrently
119
- # with a signalling thread.
99
+ # The block may run more than once and may run concurrently with a
100
+ # signalling thread. Rechecking it after registration prevents lost
101
+ # wakeups. Interrupted waits are unregistered before the exception is
102
+ # propagated.
120
103
  #
121
104
  # @yieldreturn [untyped] Truthy to wake, falsy to keep waiting
122
105
  # @return [untyped] The first truthy value returned by the block
@@ -140,15 +123,19 @@ module AtomicRuby
140
123
 
141
124
  self_thread = Thread.current
142
125
  loop do
143
- waiter = _add_waiter(self_thread)
144
- result = yield
145
- if result
146
- _remove_waiter(waiter)
147
- return result
148
- end
126
+ Thread.handle_interrupt(Exception => :never) do
127
+ waiter = _add_waiter(self_thread)
128
+ begin
129
+ Thread.handle_interrupt(Exception => :immediate) do
130
+ result = yield
131
+ return result if result
149
132
 
150
- Thread.stop
151
- _remove_waiter(waiter)
133
+ Thread.stop
134
+ end
135
+ ensure
136
+ _remove_waiter(waiter)
137
+ end
138
+ end
152
139
  end
153
140
  end
154
141
  end
@@ -133,10 +133,6 @@ module AtomicRuby
133
133
 
134
134
  # Blocks the current thread until the count reaches zero.
135
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
136
  # @return [void]
141
137
  #
142
138
  # @example Simple wait
@@ -166,7 +162,7 @@ module AtomicRuby
166
162
  #
167
163
  # @rbs () -> void
168
164
  def wait
169
- Thread.pass while @count.value > 0
165
+ sleep 0.001 while @count.value > 0
170
166
  end
171
167
  end
172
168
  end
@@ -155,12 +155,13 @@ module AtomicRuby
155
155
  #
156
156
  # @rbs (Proc work) -> void
157
157
  def <<(work)
158
- raise EnqueuedWorkAfterShutdownError if @shutdown.true?
158
+ Thread.handle_interrupt(Exception => :never) do
159
+ @trim_requested&.make_false
160
+ raise EnqueuedWorkAfterShutdownError unless @queue.push(work)
159
161
 
160
- @trim_requested&.make_false
161
- @queue.push(work)
162
- @work_available.signal
163
- @autoscale_available&.signal
162
+ @work_available.signal
163
+ @autoscale_available&.signal
164
+ end
164
165
  end
165
166
 
166
167
  # Returns the number of currently alive worker threads.
@@ -242,6 +243,9 @@ module AtomicRuby
242
243
  # 2. Waits for all currently queued work to complete
243
244
  # 3. Waits for all worker threads to terminate
244
245
  #
246
+ # Enqueues accepted before shutdown are processed before the workers
247
+ # terminate.
248
+ #
245
249
  # After shutdown, all worker threads will be terminated and the pool
246
250
  # cannot be restarted. Attempting to enqueue work after shutdown
247
251
  # will raise an exception.
@@ -258,11 +262,12 @@ module AtomicRuby
258
262
  #
259
263
  # @rbs () -> void
260
264
  def shutdown
261
- return if @shutdown.true?
262
-
263
- @shutdown.make_true
264
- @autoscale_available&.broadcast
265
- @work_available.broadcast
265
+ Thread.handle_interrupt(Exception => :never) do
266
+ @queue.send(:_close)
267
+ @shutdown.make_true
268
+ @autoscale_available&.broadcast
269
+ @work_available.broadcast
270
+ end
266
271
  @autoscaler&.join
267
272
  @threads.each(&:join)
268
273
  @thread_pool_monitor&.stop
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.15.2"
4
+ VERSION = "0.15.3"
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.15.2
4
+ version: 0.15.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young