atomic-ruby 0.15.1 → 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: af7a35affcaa2790e6944ee01eb6af8f4d92b5c2479bb1995128742a562fd395
4
- data.tar.gz: d3908277e6ef09a70f7d46a057a5db7f60c179885d980b4fbbf407bdade6b073
3
+ metadata.gz: a50be9a8594152f8d07b2338588a01be6aecadf55f993ea6cc4c2431e80e2b76
4
+ data.tar.gz: eb9c45d8dff70d0697d3d62ba5a539e3d71e42c21ea7ab6b646d44fde69f39ac
5
5
  SHA512:
6
- metadata.gz: f5a3525b194636ddb439c0c8e9597e3ed1870a8c0165bc1582cb120a45c49b30a8cf6bd60f393a9e3e0c848abf760b181dce1b9b3026199ff8ffb2ba833dcc1a
7
- data.tar.gz: e89a1f593774fc1722bfc8a9229385682e33a10df705ba78da3c67f904ab566c770b7aebdebc32069ed7f4dfc2781ad52165f08f10cf3fd4e4d723c41687e02f
6
+ metadata.gz: abde2bc85010be3634caeaf66d262acf1c5c70c50af5e9e420a43c87071965bd543a475b01b5d1b0d1569682802595e936a9d224e636896a681e5c3c85d98a77
7
+ data.tar.gz: e93d7b35c3ccdba9c82906587c24455c248655ffbe3f9d4141e4cb029705306f80c13b36f6a0ce7193a33701758ad0a9c2f77a7aa0c94ad92e49392ba899c055
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
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
+
9
+ ## [0.15.2] - 2026-08-30
10
+
11
+ - Avoid thread event hook overhead for fixed thread pools
12
+
3
13
  ## [0.15.1] - 2026-08-30
4
14
 
5
15
  - Prevent OS scheduling delays from stopping adaptive thread pool growth
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.1
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.1
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.325k (± 2.0%) i/s (754.96 μs/i) - 6.700k in 5.058203s
371
- Atomic Ruby Atomic Boolean Toggle 1.190k (± 2.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.343k (± 1.9%) i/s (744.55 μs/i) - 6.732k in 5.012307s
371
+ Atomic Ruby Atomic Boolean Toggle 1.207k (± 1.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.1
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.458k (± 4.2%) i/s (25.34 μs/i) - 198.850k in 5.039596s
452
- Atomic Ruby Atomic Condition Variable Wait/Signal 39.046k (± 4.2%) i/s (25.61 μs/i) - 198.135k in 5.074411s
451
+ Synchronized Condition Variable Wait/Signal 41.923k (± 3.5%) i/s (23.85 μs/i) - 211.224k in 5.038330s
452
+ Atomic Ruby Atomic Condition Variable Wait/Signal 40.666k (± 3.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.1
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.431k (± 6.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.493k (± 1.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.1
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.079561 seconds
594
- Atomic Ruby Atomic Thread Pool: 4.99168 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);
@@ -536,6 +546,7 @@ typedef struct {
536
546
  _Atomic unsigned long long waiting_time;
537
547
  _Atomic unsigned long long blocked_time;
538
548
  _Atomic unsigned long long running_cpu_time;
549
+ bool active;
539
550
  } atomic_ruby_thread_pool_monitor_t;
540
551
 
541
552
  typedef struct {
@@ -546,8 +557,27 @@ typedef struct {
546
557
  } atomic_ruby_thread_pool_worker_state_t;
547
558
 
548
559
  static rb_internal_thread_specific_key_t atomic_ruby_thread_pool_worker_key;
560
+ static rb_nativethread_lock_t atomic_ruby_thread_pool_monitor_lock;
561
+ static unsigned int atomic_ruby_thread_pool_monitor_count;
562
+ static rb_internal_thread_event_hook_t *atomic_ruby_thread_pool_event_hook;
563
+
564
+ static void atomic_ruby_thread_pool_event_callback(rb_event_flag_t event, const rb_internal_thread_event_data_t *event_data, void *user_data);
565
+
566
+ static void atomic_ruby_thread_pool_monitor_stop(atomic_ruby_thread_pool_monitor_t *monitor) {
567
+ rb_nativethread_lock_lock(&atomic_ruby_thread_pool_monitor_lock);
568
+ if (monitor->active) {
569
+ monitor->active = false;
570
+ atomic_ruby_thread_pool_monitor_count--;
571
+ if (atomic_ruby_thread_pool_monitor_count == 0) {
572
+ rb_internal_thread_remove_event_hook(atomic_ruby_thread_pool_event_hook);
573
+ atomic_ruby_thread_pool_event_hook = NULL;
574
+ }
575
+ }
576
+ rb_nativethread_lock_unlock(&atomic_ruby_thread_pool_monitor_lock);
577
+ }
549
578
 
550
579
  static void atomic_ruby_thread_pool_monitor_free(void *ptr) {
580
+ atomic_ruby_thread_pool_monitor_stop(ptr);
551
581
  xfree(ptr);
552
582
  }
553
583
 
@@ -655,9 +685,37 @@ static VALUE rb_cThreadPoolMonitor_allocate(VALUE klass) {
655
685
  atomic_init(&monitor->waiting_time, 0);
656
686
  atomic_init(&monitor->blocked_time, 0);
657
687
  atomic_init(&monitor->running_cpu_time, 0);
688
+ monitor->active = false;
658
689
  return obj;
659
690
  }
660
691
 
692
+ static VALUE rb_cThreadPoolMonitor_start(VALUE self) {
693
+ atomic_ruby_thread_pool_monitor_t *monitor;
694
+ TypedData_Get_Struct(self, atomic_ruby_thread_pool_monitor_t, &atomic_ruby_thread_pool_monitor_type, monitor);
695
+ rb_nativethread_lock_lock(&atomic_ruby_thread_pool_monitor_lock);
696
+ if (!monitor->active) {
697
+ monitor->active = true;
698
+ if (atomic_ruby_thread_pool_monitor_count++ == 0) {
699
+ atomic_ruby_thread_pool_event_hook = rb_internal_thread_add_event_hook(
700
+ atomic_ruby_thread_pool_event_callback,
701
+ RUBY_INTERNAL_THREAD_EVENT_READY |
702
+ RUBY_INTERNAL_THREAD_EVENT_RESUMED |
703
+ RUBY_INTERNAL_THREAD_EVENT_SUSPENDED,
704
+ NULL
705
+ );
706
+ }
707
+ }
708
+ rb_nativethread_lock_unlock(&atomic_ruby_thread_pool_monitor_lock);
709
+ return Qnil;
710
+ }
711
+
712
+ static VALUE rb_cThreadPoolMonitor_stop(VALUE self) {
713
+ atomic_ruby_thread_pool_monitor_t *monitor;
714
+ TypedData_Get_Struct(self, atomic_ruby_thread_pool_monitor_t, &atomic_ruby_thread_pool_monitor_type, monitor);
715
+ atomic_ruby_thread_pool_monitor_stop(monitor);
716
+ return Qnil;
717
+ }
718
+
661
719
  static VALUE rb_cThreadPoolMonitor_register_worker(VALUE self) {
662
720
  atomic_ruby_thread_pool_monitor_t *monitor;
663
721
  TypedData_Get_Struct(self, atomic_ruby_thread_pool_monitor_t, &atomic_ruby_thread_pool_monitor_type, monitor);
@@ -739,6 +797,7 @@ RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
739
797
  rb_define_alloc_func(rb_cAtomicQueue, rb_cAtomicQueue_allocate);
740
798
  rb_define_private_method(rb_cAtomicQueue, "_initialize", rb_cAtomicQueue_initialize, 0);
741
799
  rb_define_private_method(rb_cAtomicQueue, "_push", rb_cAtomicQueue_push, 1);
800
+ rb_define_private_method(rb_cAtomicQueue, "_close", rb_cAtomicQueue_close, 0);
742
801
  rb_define_private_method(rb_cAtomicQueue, "_pop", rb_cAtomicQueue_pop, 0);
743
802
  rb_define_private_method(rb_cAtomicQueue, "_size", rb_cAtomicQueue_size, 0);
744
803
  rb_define_private_method(rb_cAtomicQueue, "_empty_p", rb_cAtomicQueue_empty_p, 0);
@@ -756,15 +815,13 @@ RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
756
815
  rb_define_private_method(rb_cAtomicConditionVariable, "_waiter_count", rb_cAtomicConditionVariable_waiter_count, 0);
757
816
 
758
817
  atomic_ruby_thread_pool_worker_key = rb_internal_thread_specific_key_create();
759
- rb_internal_thread_add_event_hook(
760
- atomic_ruby_thread_pool_event_callback,
761
- RUBY_INTERNAL_THREAD_EVENT_READY |
762
- RUBY_INTERNAL_THREAD_EVENT_RESUMED |
763
- RUBY_INTERNAL_THREAD_EVENT_SUSPENDED,
764
- NULL
765
- );
818
+ rb_nativethread_lock_initialize(&atomic_ruby_thread_pool_monitor_lock);
819
+ atomic_ruby_thread_pool_monitor_count = 0;
820
+ atomic_ruby_thread_pool_event_hook = NULL;
766
821
  VALUE rb_cThreadPoolMonitor = rb_define_class_under(rb_mAtomicRuby, "ThreadPoolMonitor", rb_cObject);
767
822
  rb_define_alloc_func(rb_cThreadPoolMonitor, rb_cThreadPoolMonitor_allocate);
823
+ rb_define_method(rb_cThreadPoolMonitor, "start", rb_cThreadPoolMonitor_start, 0);
824
+ rb_define_method(rb_cThreadPoolMonitor, "stop", rb_cThreadPoolMonitor_stop, 0);
768
825
  rb_define_method(rb_cThreadPoolMonitor, "register_worker", rb_cThreadPoolMonitor_register_worker, 0);
769
826
  rb_define_method(rb_cThreadPoolMonitor, "unregister_worker", rb_cThreadPoolMonitor_unregister_worker, 0);
770
827
  rb_define_method(rb_cThreadPoolMonitor, "start_work", rb_cThreadPoolMonitor_start_work, 0);
@@ -4,6 +4,7 @@
4
4
  #include "ruby.h"
5
5
  #include "ruby/atomic.h"
6
6
  #include "ruby/thread.h"
7
+ #include "ruby/thread_native.h"
7
8
  #include "ruby/version.h"
8
9
 
9
10
  #include <stdatomic.h>
@@ -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
@@ -125,6 +125,7 @@ module AtomicRuby
125
125
  @autoscale_available = AtomicConditionVariable.new
126
126
  @trim_requested = AtomicBoolean.new(false)
127
127
  @thread_pool_monitor = ThreadPoolMonitor.new
128
+ @thread_pool_monitor.start
128
129
  end
129
130
 
130
131
  start
@@ -154,12 +155,13 @@ module AtomicRuby
154
155
  #
155
156
  # @rbs (Proc work) -> void
156
157
  def <<(work)
157
- raise EnqueuedWorkAfterShutdownError if @shutdown.true?
158
+ Thread.handle_interrupt(Exception => :never) do
159
+ @trim_requested&.make_false
160
+ raise EnqueuedWorkAfterShutdownError unless @queue.push(work)
158
161
 
159
- @trim_requested&.make_false
160
- @queue.push(work)
161
- @work_available.signal
162
- @autoscale_available&.signal
162
+ @work_available.signal
163
+ @autoscale_available&.signal
164
+ end
163
165
  end
164
166
 
165
167
  # Returns the number of currently alive worker threads.
@@ -241,6 +243,9 @@ module AtomicRuby
241
243
  # 2. Waits for all currently queued work to complete
242
244
  # 3. Waits for all worker threads to terminate
243
245
  #
246
+ # Enqueues accepted before shutdown are processed before the workers
247
+ # terminate.
248
+ #
244
249
  # After shutdown, all worker threads will be terminated and the pool
245
250
  # cannot be restarted. Attempting to enqueue work after shutdown
246
251
  # will raise an exception.
@@ -257,13 +262,15 @@ module AtomicRuby
257
262
  #
258
263
  # @rbs () -> void
259
264
  def shutdown
260
- return if @shutdown.true?
261
-
262
- @shutdown.make_true
263
- @autoscale_available&.broadcast
264
- @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
265
271
  @autoscaler&.join
266
272
  @threads.each(&:join)
273
+ @thread_pool_monitor&.stop
267
274
  end
268
275
 
269
276
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.15.1"
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.1
4
+ version: 0.15.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young