atomic-ruby 0.12.0 → 0.14.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: 0171c5fec65586e1a6b090b65e266a8c4fa88c5a3fa7c98e6c41c5fd63654f31
4
- data.tar.gz: aa26033345014a3d64ff81faa7ac812fc3de3d4189f9c59d0f559684429d7540
3
+ metadata.gz: 5a0461b3ac6fd6dbc7674cf028f8fb3727cad76da32ad2c6054a92a2818fe25f
4
+ data.tar.gz: 6f20eabbb25f29f6c43c166cbe240c1ddf2a6def437317c392a53fe86699a6b8
5
5
  SHA512:
6
- metadata.gz: 226c50dd465ec5f255f6b5a2991e64939bcdf77d08ecdda4f9c6aea8ee68de16cdb46ac2be7b6705417b316a40a4c4159cf21b4eef34759f0bc939a544930f50
7
- data.tar.gz: 4e80ac5fc97aed98950779897c5308d5d3f8180424cca09e4af611d980570b67b3305a1bc0557f8d4278a6b618f7c3ee69d2a60935764e5f59f7769f04711967
6
+ metadata.gz: 8e29e25f0790c2c3f3417efae11440a5f0500e0e943ce4c4a903f7fc4180342e832319cfe82712284449c876c70aa29ea22899480668bb1f35cc02c9369f8a6c
7
+ data.tar.gz: f26bc0f694a0e00cc84c9988761a1cf56c50c89d499bd70eb88b3fb896749ba36465d53d16e03d5ad636c8e8cd59b5b1f179bca827cd85c9a59397951bcd6013
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.14.0] - 2026-08-15
4
+
5
+ - Replace the `AtomicConditionVariable` waiter list with a native doubly-linked list
6
+ - Replace the two-stack queue in `AtomicThreadPool` with `AtomicQueue`
7
+ - Add `AtomicQueue`
8
+
9
+ ## [0.13.0] - 2026-06-12
10
+
11
+ - Replace `sleep` with `AtomicConditionVariable` in idle `AtomicThreadPool` workers
12
+ - Add `AtomicConditionVariable`
13
+
3
14
  ## [0.12.0] - 2026-05-18
4
15
 
5
16
  - Wake idle `AtomicThreadPool` workers on enqueue
data/README.md CHANGED
@@ -49,6 +49,48 @@ atom.toggle
49
49
  p atom.false? #=> true
50
50
  ```
51
51
 
52
+ `AtomicConditionVariable`:
53
+
54
+ ```ruby
55
+ require "atomic-ruby"
56
+
57
+ condvar = AtomicConditionVariable.new
58
+ ready = AtomicBoolean.new(false)
59
+ p condvar.waiter_count #=> 0
60
+
61
+ waiter = Thread.new do
62
+ condvar.wait { ready.true? }
63
+ end
64
+ Thread.pass until condvar.waiter_count == 1
65
+ p condvar.waiter_count #=> 1
66
+
67
+ ready.make_true
68
+ p condvar.signal #=> true
69
+ waiter.join
70
+ p condvar.waiter_count #=> 0
71
+ ```
72
+
73
+ `AtomicQueue`:
74
+
75
+ ```ruby
76
+ require "atomic-ruby"
77
+
78
+ queue = AtomicQueue.new
79
+ p queue.empty? #=> true
80
+ p queue.size #=> 0
81
+
82
+ queue.push(1)
83
+ queue << 2
84
+ queue << 3
85
+ p queue.size #=> 3
86
+
87
+ p queue.pop #=> 1
88
+ p queue.pop #=> 2
89
+ p queue.pop #=> 3
90
+ p queue.pop #=> nil
91
+ p queue.empty? #=> true
92
+ ```
93
+
52
94
  `AtomicThreadPool`:
53
95
 
54
96
  ```ruby
@@ -98,7 +140,9 @@ p latch.count #=> 0
98
140
  ```
99
141
 
100
142
  > [!NOTE]
101
- > `Atom`, `AtomicBoolean`, and `AtomicCountDownLatch` are Ractor-safe in Ruby 4.0+.
143
+ > `Atom`, `AtomicBoolean`, and `AtomicCountDownLatch` are Ractor-safe in Ruby 4.0+. `AtomicConditionVariable` and
144
+ > `AtomicQueue` are not, since they hold mutable references (parked `Thread`s and queued values, respectively) which
145
+ > cannot be shared across ractors.
102
146
 
103
147
  ## Benchmarks
104
148
 
@@ -208,11 +252,11 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
208
252
  ```
209
253
 
210
254
  ```
211
- > bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
255
+ > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
212
256
 
213
- ruby version: ruby 4.0.3 (2026-04-21 revision 85ddef263a) +YJIT +PRISM [arm64-darwin23]
257
+ ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
214
258
  concurrent-ruby version: 1.3.6
215
- atomic-ruby version: 0.11.0
259
+ atomic-ruby version: 0.14.0
216
260
 
217
261
  Balances:
218
262
  Synchronized Bank Account Balance: 975
@@ -220,9 +264,9 @@ Concurrent Ruby Atomic Bank Account Balance: 975
220
264
  Atomic Ruby Atomic Bank Account Balance: 975
221
265
 
222
266
  Benchmark Results:
223
- Synchronized Bank Account: 5.106931 seconds
224
- Concurrent Ruby Atomic Bank Account: 5.106199 seconds
225
- Atomic Ruby Atomic Bank Account: 5.101662 seconds
267
+ Synchronized Bank Account: 5.117072 seconds
268
+ Concurrent Ruby Atomic Bank Account: 5.128366 seconds
269
+ Atomic Ruby Atomic Bank Account: 5.110595 seconds
226
270
  ```
227
271
 
228
272
  </details>
@@ -299,31 +343,181 @@ end
299
343
  ```
300
344
 
301
345
  ```
302
- > bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
346
+ > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
303
347
 
304
- ruby version: ruby 4.0.3 (2026-04-21 revision 85ddef263a) +YJIT +PRISM [arm64-darwin23]
348
+ ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
305
349
  concurrent-ruby version: 1.3.6
306
- atomic-ruby version: 0.11.0
350
+ atomic-ruby version: 0.14.0
351
+
352
+ Warming up --------------------------------------
353
+ Synchronized Boolean Toggle 157.000 i/100ms
354
+ Concurrent Ruby Atomic Boolean Toggle 134.000 i/100ms
355
+ Atomic Ruby Atomic Boolean Toggle 120.000 i/100ms
356
+ Calculating -------------------------------------
357
+ Synchronized Boolean Toggle 1.629k (± 2.0%) i/s (613.87 μs/i) - 8.164k in 5.011636s
358
+ Concurrent Ruby Atomic Boolean Toggle 1.325k (± 2.0%) i/s (754.96 μs/i) - 6.700k in 5.058203s
359
+ Atomic Ruby Atomic Boolean Toggle 1.190k (± 2.6%) i/s (840.11 μs/i) - 6.000k in 5.040665s
360
+
361
+ Comparison:
362
+ Synchronized Boolean Toggle: 1629.0 i/s
363
+ Concurrent Ruby Atomic Boolean Toggle: 1324.6 i/s - 1.23x slower
364
+ Atomic Ruby Atomic Boolean Toggle: 1190.3 i/s - 1.37x slower
365
+ ```
366
+
367
+ </details>
368
+
369
+ <details>
370
+
371
+ <summary>AtomicConditionVariable</summary>
372
+
373
+ ```ruby
374
+ # frozen_string_literal: true
375
+
376
+ require "benchmark/ips"
377
+ require_relative "../lib/atomic-ruby"
378
+
379
+ module Benchmark
380
+ module IPS
381
+ class Job
382
+ class StreamReport
383
+ def start_warming
384
+ @out.puts "\n"
385
+ @out.puts "ruby version: #{RUBY_DESCRIPTION}"
386
+ @out.puts "atomic-ruby version: #{AtomicRuby::VERSION}"
387
+ @out.puts "\n"
388
+ @out.puts "Warming up --------------------------------------"
389
+ end
390
+ end
391
+ end
392
+ end
393
+ end
394
+
395
+ Benchmark.ips do |x|
396
+ x.report("Synchronized Condition Variable Wait/Signal") do
397
+ flag = false
398
+ mutex = Mutex.new
399
+ condvar = ConditionVariable.new
400
+
401
+ waiter = Thread.new do
402
+ mutex.synchronize do
403
+ condvar.wait(mutex) until flag
404
+ end
405
+ end
406
+
407
+ mutex.synchronize do
408
+ flag = true
409
+ condvar.signal
410
+ end
411
+ waiter.join
412
+ end
413
+
414
+ x.report("Atomic Ruby Atomic Condition Variable Wait/Signal") do
415
+ flag = AtomicBoolean.new(false)
416
+ condvar = AtomicConditionVariable.new
417
+
418
+ waiter = Thread.new { condvar.wait { flag.true? } }
419
+
420
+ flag.make_true
421
+ condvar.signal
422
+ waiter.join
423
+ end
424
+
425
+ x.compare!
426
+ end
427
+ ```
428
+
429
+ ```
430
+ > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_condition_variable_benchmark.rb
431
+
432
+ ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
433
+ atomic-ruby version: 0.14.0
307
434
 
308
435
  Warming up --------------------------------------
309
- Synchronized Boolean Toggle
310
- 135.000 i/100ms
311
- Concurrent Ruby Atomic Boolean Toggle
312
- 117.000 i/100ms
313
- Atomic Ruby Atomic Boolean Toggle
314
- 101.000 i/100ms
436
+ Synchronized Condition Variable Wait/Signal 3.977k i/100ms
437
+ Atomic Ruby Atomic Condition Variable Wait/Signal 3.885k i/100ms
315
438
  Calculating -------------------------------------
316
- Synchronized Boolean Toggle
317
- 1.414k1.4%) i/s (707.15 μs/i) - 7.155k in 5.060692s
318
- Concurrent Ruby Atomic Boolean Toggle
319
- 1.149k (± 2.9%) i/s (870.53 μs/i) - 5.850k in 5.097019s
320
- Atomic Ruby Atomic Boolean Toggle
321
- 1.046k (± 2.1%) i/s (955.57 μs/i) - 5.252k in 5.021118s
439
+ Synchronized Condition Variable Wait/Signal 39.458k (± 4.2%) i/s (25.34 μs/i) - 198.850k in 5.039596s
440
+ Atomic Ruby Atomic Condition Variable Wait/Signal 39.046k4.2%) i/s (25.61 μs/i) - 198.135k in 5.074411s
322
441
 
323
442
  Comparison:
324
- Synchronized Boolean Toggle: 1414.1 i/s
325
- Concurrent Ruby Atomic Boolean Toggle: 1148.7 i/s - 1.23x slower
326
- Atomic Ruby Atomic Boolean Toggle: 1046.5 i/s - 1.35x slower
443
+ Synchronized Condition Variable Wait/Signal: 39457.5 i/s
444
+ Atomic Ruby Atomic Condition Variable Wait/Signal: 39045.9 i/s - same-ish: difference falls within error
445
+ ```
446
+
447
+ </details>
448
+
449
+ <details>
450
+
451
+ <summary>AtomicQueue</summary>
452
+
453
+ ```ruby
454
+ # frozen_string_literal: true
455
+
456
+ require "benchmark/ips"
457
+ require_relative "../lib/atomic-ruby"
458
+
459
+ module Benchmark
460
+ module IPS
461
+ class Job
462
+ class StreamReport
463
+ def start_warming
464
+ @out.puts "\n"
465
+ @out.puts "ruby version: #{RUBY_DESCRIPTION}"
466
+ @out.puts "atomic-ruby version: #{AtomicRuby::VERSION}"
467
+ @out.puts "\n"
468
+ @out.puts "Warming up --------------------------------------"
469
+ end
470
+ end
471
+ end
472
+ end
473
+ end
474
+
475
+ Benchmark.ips do |x|
476
+ x.report("Synchronized Queue Push/Pop") do
477
+ queue = Thread::Queue.new
478
+ 20.times.map do
479
+ Thread.new do
480
+ 100.times do |i|
481
+ queue.push(i)
482
+ queue.pop(true) rescue nil
483
+ end
484
+ end
485
+ end.each(&:join)
486
+ end
487
+
488
+ x.report("Atomic Ruby Atomic Queue Push/Pop") do
489
+ queue = AtomicQueue.new
490
+ 20.times.map do
491
+ Thread.new do
492
+ 100.times do |i|
493
+ queue.push(i)
494
+ queue.pop
495
+ end
496
+ end
497
+ end.each(&:join)
498
+ end
499
+
500
+ x.compare!
501
+ end
502
+ ```
503
+
504
+ ```
505
+ > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_queue_benchmark.rb
506
+
507
+ ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
508
+ atomic-ruby version: 0.14.0
509
+
510
+ Warming up --------------------------------------
511
+ Synchronized Queue Push/Pop 181.000 i/100ms
512
+ Atomic Ruby Atomic Queue Push/Pop 132.000 i/100ms
513
+ Calculating -------------------------------------
514
+ Synchronized Queue Push/Pop 1.845k (± 2.1%) i/s (541.91 μs/i) - 9.231k in 5.002394s
515
+ Atomic Ruby Atomic Queue Push/Pop 1.431k (± 6.8%) i/s (698.64 μs/i) - 7.260k in 5.072143s
516
+
517
+ Comparison:
518
+ Synchronized Queue Push/Pop: 1845.3 i/s
519
+ Atomic Ruby Atomic Queue Push/Pop: 1431.3 i/s - 1.29x slower
520
+
327
521
  ```
328
522
 
329
523
  </details>
@@ -377,15 +571,15 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
377
571
  ```
378
572
 
379
573
  ```
380
- > bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
574
+ > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
381
575
 
382
- ruby version: ruby 4.0.4 (2026-05-12 revision b89eb1bcbf) +YJIT +PRISM [arm64-darwin23]
576
+ ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
383
577
  concurrent-ruby version: 1.3.6
384
- atomic-ruby version: 0.12.0
578
+ atomic-ruby version: 0.14.0
385
579
 
386
580
  Benchmark Results:
387
- Concurrent Ruby Thread Pool: 5.987538 seconds
388
- Atomic Ruby Atomic Thread Pool: 5.528936 seconds
581
+ Concurrent Ruby Thread Pool: 5.078227 seconds
582
+ Atomic Ruby Atomic Thread Pool: 4.824681 seconds
389
583
  ```
390
584
 
391
585
  </details>
@@ -122,6 +122,405 @@ static VALUE rb_cAtom_initialized_ractor(VALUE self) {
122
122
  }
123
123
  #endif
124
124
 
125
+ typedef struct {
126
+ volatile VALUE value;
127
+ volatile VALUE next;
128
+ } atomic_ruby_queue_node_t;
129
+
130
+ static void atomic_ruby_queue_node_mark(void *ptr) {
131
+ atomic_ruby_queue_node_t *atomic_ruby_queue_node = (atomic_ruby_queue_node_t *)ptr;
132
+ rb_gc_mark_movable(atomic_ruby_queue_node->value);
133
+ rb_gc_mark_movable(atomic_ruby_queue_node->next);
134
+ }
135
+
136
+ static void atomic_ruby_queue_node_free(void *ptr) {
137
+ xfree(ptr);
138
+ }
139
+
140
+ static size_t atomic_ruby_queue_node_memsize(const void *ptr) {
141
+ return sizeof(atomic_ruby_queue_node_t);
142
+ }
143
+
144
+ static void atomic_ruby_queue_node_compact(void *ptr) {
145
+ atomic_ruby_queue_node_t *atomic_ruby_queue_node = (atomic_ruby_queue_node_t *)ptr;
146
+ atomic_ruby_queue_node->value = rb_gc_location(atomic_ruby_queue_node->value);
147
+ atomic_ruby_queue_node->next = rb_gc_location(atomic_ruby_queue_node->next);
148
+ }
149
+
150
+ static const rb_data_type_t atomic_ruby_queue_node_type = {
151
+ .wrap_struct_name = "AtomicRuby::AtomicQueue::Node",
152
+ .function = {
153
+ .dmark = atomic_ruby_queue_node_mark,
154
+ .dfree = atomic_ruby_queue_node_free,
155
+ .dsize = atomic_ruby_queue_node_memsize,
156
+ .dcompact = atomic_ruby_queue_node_compact
157
+ },
158
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
159
+ };
160
+
161
+ static VALUE rb_cAtomicQueueNode;
162
+
163
+ static VALUE atomic_ruby_queue_node_new(VALUE value, VALUE next) {
164
+ atomic_ruby_queue_node_t *atomic_ruby_queue_node;
165
+ VALUE obj = TypedData_Make_Struct(rb_cAtomicQueueNode, atomic_ruby_queue_node_t, &atomic_ruby_queue_node_type, atomic_ruby_queue_node);
166
+ RB_OBJ_WRITE(obj, &atomic_ruby_queue_node->value, value);
167
+ RB_OBJ_WRITE(obj, &atomic_ruby_queue_node->next, next);
168
+ return obj;
169
+ }
170
+
171
+ typedef struct {
172
+ volatile VALUE head;
173
+ volatile VALUE tail;
174
+ volatile rb_atomic_t count;
175
+ } atomic_ruby_queue_t;
176
+
177
+ static void atomic_ruby_queue_mark(void *ptr) {
178
+ atomic_ruby_queue_t *atomic_ruby_queue = (atomic_ruby_queue_t *)ptr;
179
+ rb_gc_mark_movable(atomic_ruby_queue->head);
180
+ rb_gc_mark_movable(atomic_ruby_queue->tail);
181
+ }
182
+
183
+ static void atomic_ruby_queue_free(void *ptr) {
184
+ xfree(ptr);
185
+ }
186
+
187
+ static size_t atomic_ruby_queue_memsize(const void *ptr) {
188
+ return sizeof(atomic_ruby_queue_t);
189
+ }
190
+
191
+ static void atomic_ruby_queue_compact(void *ptr) {
192
+ atomic_ruby_queue_t *atomic_ruby_queue = (atomic_ruby_queue_t *)ptr;
193
+ atomic_ruby_queue->head = rb_gc_location(atomic_ruby_queue->head);
194
+ atomic_ruby_queue->tail = rb_gc_location(atomic_ruby_queue->tail);
195
+ }
196
+
197
+ static const rb_data_type_t atomic_ruby_queue_type = {
198
+ .wrap_struct_name = "AtomicRuby::AtomicQueue",
199
+ .function = {
200
+ .dmark = atomic_ruby_queue_mark,
201
+ .dfree = atomic_ruby_queue_free,
202
+ .dsize = atomic_ruby_queue_memsize,
203
+ .dcompact = atomic_ruby_queue_compact
204
+ },
205
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
206
+ };
207
+
208
+ static VALUE rb_cAtomicQueue_allocate(VALUE klass) {
209
+ atomic_ruby_queue_t *atomic_ruby_queue;
210
+ VALUE obj = TypedData_Make_Struct(klass, atomic_ruby_queue_t, &atomic_ruby_queue_type, atomic_ruby_queue);
211
+ RB_OBJ_WRITE(obj, &atomic_ruby_queue->head, Qnil);
212
+ RB_OBJ_WRITE(obj, &atomic_ruby_queue->tail, Qnil);
213
+ atomic_ruby_queue->count = 0;
214
+ return obj;
215
+ }
216
+
217
+ static VALUE rb_cAtomicQueue_initialize(VALUE self) {
218
+ atomic_ruby_queue_t *atomic_ruby_queue;
219
+ TypedData_Get_Struct(self, atomic_ruby_queue_t, &atomic_ruby_queue_type, atomic_ruby_queue);
220
+
221
+ VALUE sentinel = atomic_ruby_queue_node_new(Qnil, Qnil);
222
+ RB_OBJ_WRITE(self, &atomic_ruby_queue->head, sentinel);
223
+ RB_OBJ_WRITE(self, &atomic_ruby_queue->tail, sentinel);
224
+
225
+ return self;
226
+ }
227
+
228
+ static VALUE rb_cAtomicQueue_push(VALUE self, VALUE value) {
229
+ atomic_ruby_queue_t *atomic_ruby_queue;
230
+ TypedData_Get_Struct(self, atomic_ruby_queue_t, &atomic_ruby_queue_type, atomic_ruby_queue);
231
+
232
+ VALUE new_node = atomic_ruby_queue_node_new(value, Qnil);
233
+
234
+ VALUE tail;
235
+ while (1) {
236
+ tail = (VALUE)RUBY_ATOMIC_PTR_LOAD(atomic_ruby_queue->tail);
237
+ atomic_ruby_queue_node_t *tail_node;
238
+ TypedData_Get_Struct(tail, atomic_ruby_queue_node_t, &atomic_ruby_queue_node_type, tail_node);
239
+ VALUE tail_next = (VALUE)RUBY_ATOMIC_PTR_LOAD(tail_node->next);
240
+
241
+ if (tail != (VALUE)RUBY_ATOMIC_PTR_LOAD(atomic_ruby_queue->tail)) continue;
242
+
243
+ if (tail_next != Qnil) {
244
+ RUBY_ATOMIC_VALUE_CAS(atomic_ruby_queue->tail, tail, tail_next);
245
+ RB_OBJ_WRITTEN(self, tail, tail_next);
246
+ continue;
247
+ }
248
+
249
+ if (RUBY_ATOMIC_VALUE_CAS(tail_node->next, Qnil, new_node) == Qnil) {
250
+ RB_OBJ_WRITTEN(tail, Qnil, new_node);
251
+ break;
252
+ }
253
+ }
254
+
255
+ RUBY_ATOMIC_VALUE_CAS(atomic_ruby_queue->tail, tail, new_node);
256
+ RB_OBJ_WRITTEN(self, tail, new_node);
257
+ RUBY_ATOMIC_INC(atomic_ruby_queue->count);
258
+
259
+ return self;
260
+ }
261
+
262
+ static VALUE rb_cAtomicQueue_pop(VALUE self) {
263
+ atomic_ruby_queue_t *atomic_ruby_queue;
264
+ TypedData_Get_Struct(self, atomic_ruby_queue_t, &atomic_ruby_queue_type, atomic_ruby_queue);
265
+
266
+ VALUE head, head_next, result;
267
+ while (1) {
268
+ head = (VALUE)RUBY_ATOMIC_PTR_LOAD(atomic_ruby_queue->head);
269
+ VALUE tail = (VALUE)RUBY_ATOMIC_PTR_LOAD(atomic_ruby_queue->tail);
270
+ atomic_ruby_queue_node_t *head_node;
271
+ TypedData_Get_Struct(head, atomic_ruby_queue_node_t, &atomic_ruby_queue_node_type, head_node);
272
+ head_next = (VALUE)RUBY_ATOMIC_PTR_LOAD(head_node->next);
273
+
274
+ if (head != (VALUE)RUBY_ATOMIC_PTR_LOAD(atomic_ruby_queue->head)) continue;
275
+
276
+ if (head == tail) {
277
+ if (head_next == Qnil) return Qnil;
278
+
279
+ RUBY_ATOMIC_VALUE_CAS(atomic_ruby_queue->tail, tail, head_next);
280
+ RB_OBJ_WRITTEN(self, tail, head_next);
281
+ continue;
282
+ }
283
+
284
+ atomic_ruby_queue_node_t *next_node;
285
+ TypedData_Get_Struct(head_next, atomic_ruby_queue_node_t, &atomic_ruby_queue_node_type, next_node);
286
+ result = next_node->value;
287
+
288
+ if (RUBY_ATOMIC_VALUE_CAS(atomic_ruby_queue->head, head, head_next) == head) {
289
+ RB_OBJ_WRITTEN(self, head, head_next);
290
+ break;
291
+ }
292
+ }
293
+
294
+ RUBY_ATOMIC_DEC(atomic_ruby_queue->count);
295
+ return result;
296
+ }
297
+
298
+ static VALUE rb_cAtomicQueue_size(VALUE self) {
299
+ atomic_ruby_queue_t *atomic_ruby_queue;
300
+ TypedData_Get_Struct(self, atomic_ruby_queue_t, &atomic_ruby_queue_type, atomic_ruby_queue);
301
+ return UINT2NUM((unsigned int)RUBY_ATOMIC_LOAD(atomic_ruby_queue->count));
302
+ }
303
+
304
+ static VALUE rb_cAtomicQueue_empty_p(VALUE self) {
305
+ atomic_ruby_queue_t *atomic_ruby_queue;
306
+ TypedData_Get_Struct(self, atomic_ruby_queue_t, &atomic_ruby_queue_type, atomic_ruby_queue);
307
+ return RUBY_ATOMIC_LOAD(atomic_ruby_queue->count) == 0 ? Qtrue : Qfalse;
308
+ }
309
+
310
+ typedef struct {
311
+ volatile VALUE prev;
312
+ volatile VALUE next;
313
+ volatile VALUE thread;
314
+ } atomic_ruby_condition_variable_waiter_t;
315
+
316
+ static void atomic_ruby_condition_variable_waiter_mark(void *ptr) {
317
+ atomic_ruby_condition_variable_waiter_t *atomic_ruby_condition_variable_waiter = (atomic_ruby_condition_variable_waiter_t *)ptr;
318
+ rb_gc_mark_movable(atomic_ruby_condition_variable_waiter->prev);
319
+ rb_gc_mark_movable(atomic_ruby_condition_variable_waiter->next);
320
+ rb_gc_mark_movable(atomic_ruby_condition_variable_waiter->thread);
321
+ }
322
+
323
+ static void atomic_ruby_condition_variable_waiter_free(void *ptr) {
324
+ xfree(ptr);
325
+ }
326
+
327
+ static size_t atomic_ruby_condition_variable_waiter_memsize(const void *ptr) {
328
+ return sizeof(atomic_ruby_condition_variable_waiter_t);
329
+ }
330
+
331
+ static void atomic_ruby_condition_variable_waiter_compact(void *ptr) {
332
+ atomic_ruby_condition_variable_waiter_t *atomic_ruby_condition_variable_waiter = (atomic_ruby_condition_variable_waiter_t *)ptr;
333
+ atomic_ruby_condition_variable_waiter->prev = rb_gc_location(atomic_ruby_condition_variable_waiter->prev);
334
+ atomic_ruby_condition_variable_waiter->next = rb_gc_location(atomic_ruby_condition_variable_waiter->next);
335
+ atomic_ruby_condition_variable_waiter->thread = rb_gc_location(atomic_ruby_condition_variable_waiter->thread);
336
+ }
337
+
338
+ static const rb_data_type_t atomic_ruby_condition_variable_waiter_type = {
339
+ .wrap_struct_name = "AtomicRuby::AtomicConditionVariable::Waiter",
340
+ .function = {
341
+ .dmark = atomic_ruby_condition_variable_waiter_mark,
342
+ .dfree = atomic_ruby_condition_variable_waiter_free,
343
+ .dsize = atomic_ruby_condition_variable_waiter_memsize,
344
+ .dcompact = atomic_ruby_condition_variable_waiter_compact
345
+ },
346
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
347
+ };
348
+
349
+ static VALUE rb_cAtomicConditionVariableWaiter;
350
+
351
+ static VALUE atomic_ruby_condition_variable_waiter_new(VALUE thread) {
352
+ atomic_ruby_condition_variable_waiter_t *atomic_ruby_condition_variable_waiter;
353
+ VALUE obj = TypedData_Make_Struct(rb_cAtomicConditionVariableWaiter, atomic_ruby_condition_variable_waiter_t, &atomic_ruby_condition_variable_waiter_type, atomic_ruby_condition_variable_waiter);
354
+ RB_OBJ_WRITE(obj, &atomic_ruby_condition_variable_waiter->prev, Qnil);
355
+ RB_OBJ_WRITE(obj, &atomic_ruby_condition_variable_waiter->next, Qnil);
356
+ RB_OBJ_WRITE(obj, &atomic_ruby_condition_variable_waiter->thread, thread);
357
+ return obj;
358
+ }
359
+
360
+ typedef struct {
361
+ volatile VALUE head;
362
+ volatile VALUE tail;
363
+ volatile rb_atomic_t count;
364
+ } atomic_ruby_condition_variable_t;
365
+
366
+ static void atomic_ruby_condition_variable_mark(void *ptr) {
367
+ atomic_ruby_condition_variable_t *atomic_ruby_condition_variable = (atomic_ruby_condition_variable_t *)ptr;
368
+ rb_gc_mark_movable(atomic_ruby_condition_variable->head);
369
+ rb_gc_mark_movable(atomic_ruby_condition_variable->tail);
370
+ }
371
+
372
+ static void atomic_ruby_condition_variable_free(void *ptr) {
373
+ xfree(ptr);
374
+ }
375
+
376
+ static size_t atomic_ruby_condition_variable_memsize(const void *ptr) {
377
+ return sizeof(atomic_ruby_condition_variable_t);
378
+ }
379
+
380
+ static void atomic_ruby_condition_variable_compact(void *ptr) {
381
+ atomic_ruby_condition_variable_t *atomic_ruby_condition_variable = (atomic_ruby_condition_variable_t *)ptr;
382
+ atomic_ruby_condition_variable->head = rb_gc_location(atomic_ruby_condition_variable->head);
383
+ atomic_ruby_condition_variable->tail = rb_gc_location(atomic_ruby_condition_variable->tail);
384
+ }
385
+
386
+ static const rb_data_type_t atomic_ruby_condition_variable_type = {
387
+ .wrap_struct_name = "AtomicRuby::AtomicConditionVariable",
388
+ .function = {
389
+ .dmark = atomic_ruby_condition_variable_mark,
390
+ .dfree = atomic_ruby_condition_variable_free,
391
+ .dsize = atomic_ruby_condition_variable_memsize,
392
+ .dcompact = atomic_ruby_condition_variable_compact
393
+ },
394
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
395
+ };
396
+
397
+ static VALUE rb_cAtomicConditionVariable_allocate(VALUE klass) {
398
+ atomic_ruby_condition_variable_t *atomic_ruby_condition_variable;
399
+ VALUE obj = TypedData_Make_Struct(klass, atomic_ruby_condition_variable_t, &atomic_ruby_condition_variable_type, atomic_ruby_condition_variable);
400
+ RB_OBJ_WRITE(obj, &atomic_ruby_condition_variable->head, Qnil);
401
+ RB_OBJ_WRITE(obj, &atomic_ruby_condition_variable->tail, Qnil);
402
+ atomic_ruby_condition_variable->count = 0;
403
+ return obj;
404
+ }
405
+
406
+ static VALUE rb_cAtomicConditionVariable_initialize(VALUE self) {
407
+ return self;
408
+ }
409
+
410
+ static VALUE rb_cAtomicConditionVariable_add_waiter(VALUE self, VALUE thread) {
411
+ atomic_ruby_condition_variable_t *atomic_ruby_condition_variable;
412
+ TypedData_Get_Struct(self, atomic_ruby_condition_variable_t, &atomic_ruby_condition_variable_type, atomic_ruby_condition_variable);
413
+
414
+ VALUE new_waiter = atomic_ruby_condition_variable_waiter_new(thread);
415
+ atomic_ruby_condition_variable_waiter_t *new_waiter_data;
416
+ TypedData_Get_Struct(new_waiter, atomic_ruby_condition_variable_waiter_t, &atomic_ruby_condition_variable_waiter_type, new_waiter_data);
417
+
418
+ VALUE tail = atomic_ruby_condition_variable->tail;
419
+ if (tail == Qnil) {
420
+ RB_OBJ_WRITE(self, &atomic_ruby_condition_variable->head, new_waiter);
421
+ RB_OBJ_WRITE(self, &atomic_ruby_condition_variable->tail, new_waiter);
422
+ } else {
423
+ atomic_ruby_condition_variable_waiter_t *tail_data;
424
+ TypedData_Get_Struct(tail, atomic_ruby_condition_variable_waiter_t, &atomic_ruby_condition_variable_waiter_type, tail_data);
425
+ RB_OBJ_WRITE(tail, &tail_data->next, new_waiter);
426
+ RB_OBJ_WRITE(new_waiter, &new_waiter_data->prev, tail);
427
+ RB_OBJ_WRITE(self, &atomic_ruby_condition_variable->tail, new_waiter);
428
+ }
429
+ RUBY_ATOMIC_INC(atomic_ruby_condition_variable->count);
430
+
431
+ return new_waiter;
432
+ }
433
+
434
+ static VALUE rb_cAtomicConditionVariable_remove_waiter(VALUE self, VALUE waiter) {
435
+ atomic_ruby_condition_variable_t *atomic_ruby_condition_variable;
436
+ TypedData_Get_Struct(self, atomic_ruby_condition_variable_t, &atomic_ruby_condition_variable_type, atomic_ruby_condition_variable);
437
+ atomic_ruby_condition_variable_waiter_t *waiter_data;
438
+ TypedData_Get_Struct(waiter, atomic_ruby_condition_variable_waiter_t, &atomic_ruby_condition_variable_waiter_type, waiter_data);
439
+
440
+ VALUE prev = waiter_data->prev;
441
+ VALUE next = waiter_data->next;
442
+
443
+ if (prev == Qnil && next == Qnil && atomic_ruby_condition_variable->head != waiter) return Qnil;
444
+
445
+ if (prev == Qnil) {
446
+ RB_OBJ_WRITE(self, &atomic_ruby_condition_variable->head, next);
447
+ } else {
448
+ atomic_ruby_condition_variable_waiter_t *prev_data;
449
+ TypedData_Get_Struct(prev, atomic_ruby_condition_variable_waiter_t, &atomic_ruby_condition_variable_waiter_type, prev_data);
450
+ RB_OBJ_WRITE(prev, &prev_data->next, next);
451
+ }
452
+
453
+ if (next == Qnil) {
454
+ RB_OBJ_WRITE(self, &atomic_ruby_condition_variable->tail, prev);
455
+ } else {
456
+ atomic_ruby_condition_variable_waiter_t *next_data;
457
+ TypedData_Get_Struct(next, atomic_ruby_condition_variable_waiter_t, &atomic_ruby_condition_variable_waiter_type, next_data);
458
+ RB_OBJ_WRITE(next, &next_data->prev, prev);
459
+ }
460
+
461
+ RB_OBJ_WRITE(waiter, &waiter_data->prev, Qnil);
462
+ RB_OBJ_WRITE(waiter, &waiter_data->next, Qnil);
463
+ RUBY_ATOMIC_DEC(atomic_ruby_condition_variable->count);
464
+
465
+ return Qnil;
466
+ }
467
+
468
+ static VALUE rb_cAtomicConditionVariable_shift_thread(VALUE self) {
469
+ atomic_ruby_condition_variable_t *atomic_ruby_condition_variable;
470
+ TypedData_Get_Struct(self, atomic_ruby_condition_variable_t, &atomic_ruby_condition_variable_type, atomic_ruby_condition_variable);
471
+
472
+ VALUE head = atomic_ruby_condition_variable->head;
473
+ if (head == Qnil) return Qnil;
474
+
475
+ atomic_ruby_condition_variable_waiter_t *head_data;
476
+ TypedData_Get_Struct(head, atomic_ruby_condition_variable_waiter_t, &atomic_ruby_condition_variable_waiter_type, head_data);
477
+ VALUE thread = head_data->thread;
478
+ VALUE next = head_data->next;
479
+
480
+ RB_OBJ_WRITE(self, &atomic_ruby_condition_variable->head, next);
481
+ if (next == Qnil) {
482
+ RB_OBJ_WRITE(self, &atomic_ruby_condition_variable->tail, Qnil);
483
+ } else {
484
+ atomic_ruby_condition_variable_waiter_t *next_data;
485
+ TypedData_Get_Struct(next, atomic_ruby_condition_variable_waiter_t, &atomic_ruby_condition_variable_waiter_type, next_data);
486
+ RB_OBJ_WRITE(next, &next_data->prev, Qnil);
487
+ }
488
+
489
+ RB_OBJ_WRITE(head, &head_data->prev, Qnil);
490
+ RB_OBJ_WRITE(head, &head_data->next, Qnil);
491
+ RUBY_ATOMIC_DEC(atomic_ruby_condition_variable->count);
492
+
493
+ return thread;
494
+ }
495
+
496
+ static VALUE rb_cAtomicConditionVariable_drain_threads(VALUE self) {
497
+ atomic_ruby_condition_variable_t *atomic_ruby_condition_variable;
498
+ TypedData_Get_Struct(self, atomic_ruby_condition_variable_t, &atomic_ruby_condition_variable_type, atomic_ruby_condition_variable);
499
+
500
+ VALUE threads = rb_ary_new();
501
+ VALUE current = atomic_ruby_condition_variable->head;
502
+ while (current != Qnil) {
503
+ atomic_ruby_condition_variable_waiter_t *waiter_data;
504
+ TypedData_Get_Struct(current, atomic_ruby_condition_variable_waiter_t, &atomic_ruby_condition_variable_waiter_type, waiter_data);
505
+ rb_ary_push(threads, waiter_data->thread);
506
+ VALUE next = waiter_data->next;
507
+ RB_OBJ_WRITE(current, &waiter_data->prev, Qnil);
508
+ RB_OBJ_WRITE(current, &waiter_data->next, Qnil);
509
+ current = next;
510
+ }
511
+ RB_OBJ_WRITE(self, &atomic_ruby_condition_variable->head, Qnil);
512
+ RB_OBJ_WRITE(self, &atomic_ruby_condition_variable->tail, Qnil);
513
+ atomic_ruby_condition_variable->count = 0;
514
+
515
+ return threads;
516
+ }
517
+
518
+ static VALUE rb_cAtomicConditionVariable_waiter_count(VALUE self) {
519
+ atomic_ruby_condition_variable_t *atomic_ruby_condition_variable;
520
+ TypedData_Get_Struct(self, atomic_ruby_condition_variable_t, &atomic_ruby_condition_variable_type, atomic_ruby_condition_variable);
521
+ return UINT2NUM((unsigned int)RUBY_ATOMIC_LOAD(atomic_ruby_condition_variable->count));
522
+ }
523
+
125
524
  RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
126
525
  #ifdef ATOMIC_RUBY_RACTOR_SAFE
127
526
  rb_ext_ractor_safe(true);
@@ -141,4 +540,27 @@ RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
141
540
  #else
142
541
  rb_define_const(rb_mAtomicRuby, "RACTOR_SAFE", Qfalse);
143
542
  #endif
543
+
544
+ VALUE rb_cAtomicQueue = rb_define_class_under(rb_mAtomicRuby, "AtomicQueue", rb_cObject);
545
+ rb_cAtomicQueueNode = rb_define_class_under(rb_cAtomicQueue, "Node", rb_cObject);
546
+ rb_undef_alloc_func(rb_cAtomicQueueNode);
547
+
548
+ rb_define_alloc_func(rb_cAtomicQueue, rb_cAtomicQueue_allocate);
549
+ rb_define_private_method(rb_cAtomicQueue, "_initialize", rb_cAtomicQueue_initialize, 0);
550
+ rb_define_private_method(rb_cAtomicQueue, "_push", rb_cAtomicQueue_push, 1);
551
+ rb_define_private_method(rb_cAtomicQueue, "_pop", rb_cAtomicQueue_pop, 0);
552
+ rb_define_private_method(rb_cAtomicQueue, "_size", rb_cAtomicQueue_size, 0);
553
+ rb_define_private_method(rb_cAtomicQueue, "_empty_p", rb_cAtomicQueue_empty_p, 0);
554
+
555
+ VALUE rb_cAtomicConditionVariable = rb_define_class_under(rb_mAtomicRuby, "AtomicConditionVariable", rb_cObject);
556
+ rb_cAtomicConditionVariableWaiter = rb_define_class_under(rb_cAtomicConditionVariable, "Waiter", rb_cObject);
557
+ rb_undef_alloc_func(rb_cAtomicConditionVariableWaiter);
558
+
559
+ rb_define_alloc_func(rb_cAtomicConditionVariable, rb_cAtomicConditionVariable_allocate);
560
+ rb_define_private_method(rb_cAtomicConditionVariable, "_initialize", rb_cAtomicConditionVariable_initialize, 0);
561
+ rb_define_private_method(rb_cAtomicConditionVariable, "_add_waiter", rb_cAtomicConditionVariable_add_waiter, 1);
562
+ rb_define_private_method(rb_cAtomicConditionVariable, "_remove_waiter", rb_cAtomicConditionVariable_remove_waiter, 1);
563
+ rb_define_private_method(rb_cAtomicConditionVariable, "_shift_thread", rb_cAtomicConditionVariable_shift_thread, 0);
564
+ rb_define_private_method(rb_cAtomicConditionVariable, "_drain_threads", rb_cAtomicConditionVariable_drain_threads, 0);
565
+ rb_define_private_method(rb_cAtomicConditionVariable, "_waiter_count", rb_cAtomicConditionVariable_waiter_count, 0);
144
566
  }
@@ -0,0 +1,157 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "atomic_ruby/atomic_ruby"
5
+
6
+ module AtomicRuby
7
+ # Provides lock-free wait/signal coordination using atomic operations.
8
+ #
9
+ # AtomicConditionVariable lets one or more threads park until another
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.
17
+ #
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.
24
+ #
25
+ # @example Basic usage
26
+ # condvar = AtomicConditionVariable.new
27
+ # ready = AtomicBoolean.new(false)
28
+ #
29
+ # waiter = Thread.new do
30
+ # condvar.wait { ready.true? }
31
+ # puts "ready"
32
+ # end
33
+ #
34
+ # ready.make_true
35
+ # condvar.signal
36
+ #
37
+ # @example Worker loop draining an atomic queue
38
+ # condvar.wait do
39
+ # work = queue.pop
40
+ # work || shutdown.true?
41
+ # end
42
+ #
43
+ # @note This class is NOT Ractor-safe as it parks `Thread` references,
44
+ # which cannot be shared across ractors.
45
+ class AtomicConditionVariable
46
+ # Creates a new condition variable with no parked threads.
47
+ #
48
+ # @example
49
+ # condvar = AtomicConditionVariable.new
50
+ #
51
+ # @rbs () -> void
52
+ def initialize
53
+ _initialize
54
+ end
55
+
56
+ # Returns the number of currently parked waiters.
57
+ #
58
+ # This operation is atomic and thread-safe. The returned value reflects
59
+ # the state at the time of the call, but may change immediately after
60
+ # in concurrent environments.
61
+ #
62
+ # @return [Integer] The number of currently parked waiters
63
+ #
64
+ # @example
65
+ # condvar = AtomicConditionVariable.new
66
+ # puts condvar.waiter_count #=> 0
67
+ #
68
+ # @rbs () -> Integer
69
+ def waiter_count
70
+ _waiter_count
71
+ end
72
+
73
+ # Wakes one parked waiter, or no-ops if none are parked.
74
+ #
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
+ # @return [true, false] true if a waiter was signalled, false otherwise
80
+ #
81
+ # @example
82
+ # condvar = AtomicConditionVariable.new
83
+ # condvar.signal #=> false
84
+ #
85
+ # @rbs () -> bool
86
+ def signal
87
+ thread = _shift_thread
88
+ return false unless thread
89
+
90
+ thread.wakeup rescue nil
91
+ true
92
+ end
93
+
94
+ # Wakes every parked waiter.
95
+ #
96
+ # Each woken thread observes the wake the same way as with {#signal}.
97
+ #
98
+ # @return [Integer] The number of waiters signalled
99
+ #
100
+ # @example
101
+ # condvar = AtomicConditionVariable.new
102
+ # condvar.broadcast #=> 0
103
+ #
104
+ # @rbs () -> Integer
105
+ def broadcast
106
+ threads = _drain_threads
107
+ threads.each { |thread| thread.wakeup rescue nil }
108
+ threads.size
109
+ end
110
+
111
+ # Blocks until the given block returns a truthy value, then returns that
112
+ # value.
113
+ #
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.
120
+ #
121
+ # @yieldreturn [untyped] Truthy to wake, falsy to keep waiting
122
+ # @return [untyped] The first truthy value returned by the block
123
+ #
124
+ # @example Simple wait
125
+ # condvar = AtomicConditionVariable.new
126
+ # ready = AtomicBoolean.new(false)
127
+ #
128
+ # Thread.new do
129
+ # sleep(1)
130
+ # ready.make_true
131
+ # condvar.signal
132
+ # end
133
+ #
134
+ # condvar.wait { ready.true? } #=> true
135
+ #
136
+ # @rbs () { () -> untyped } -> untyped
137
+ def wait
138
+ result = yield
139
+ return result if result
140
+
141
+ self_thread = Thread.current
142
+ loop do
143
+ waiter = _add_waiter(self_thread)
144
+ result = yield
145
+ if result
146
+ _remove_waiter(waiter)
147
+ return result
148
+ end
149
+
150
+ Thread.stop
151
+ _remove_waiter(waiter)
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ AtomicConditionVariable = AtomicRuby::AtomicConditionVariable
@@ -0,0 +1,137 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "atomic_ruby/atomic_ruby"
5
+
6
+ module AtomicRuby
7
+ # Provides a lock-free FIFO queue using atomic operations.
8
+ #
9
+ # AtomicQueue is a multi-producer, multi-consumer queue backed by a
10
+ # singly-linked list of nodes with atomic head and tail pointers.
11
+ # Enqueueing and dequeueing use compare-and-swap operations so
12
+ # concurrent producers and consumers never block one another.
13
+ #
14
+ # The queue is implemented as a Michael-Scott lock-free FIFO with a
15
+ # dummy sentinel node. Both {#push} and {#pop} are O(1). Nodes are
16
+ # Ruby-managed values, so the garbage collector reclaims dequeued
17
+ # nodes once no thread holds a reference to them.
18
+ #
19
+ # @example Basic usage
20
+ # queue = AtomicQueue.new
21
+ # queue.push(1)
22
+ # queue.push(2)
23
+ # puts queue.pop #=> 1
24
+ # puts queue.pop #=> 2
25
+ # puts queue.pop #=> nil
26
+ #
27
+ # @example Producer/consumer coordination
28
+ # queue = AtomicQueue.new
29
+ # producers = 4.times.map do
30
+ # Thread.new { 100.times { |index| queue.push(index) } }
31
+ # end
32
+ # producers.each(&:join)
33
+ # puts queue.size #=> 400
34
+ #
35
+ # @note This class is NOT Ractor-safe as it stores mutable references
36
+ # to queued values that cannot be safely shared across ractors.
37
+ class AtomicQueue
38
+ # Creates a new empty queue.
39
+ #
40
+ # @example
41
+ # queue = AtomicQueue.new
42
+ #
43
+ # @rbs () -> void
44
+ def initialize
45
+ _initialize
46
+ end
47
+
48
+ # Enqueues a value at the tail of the queue.
49
+ #
50
+ # This operation is atomic and thread-safe. Multiple threads may
51
+ # push concurrently without blocking one another.
52
+ #
53
+ # @param value [untyped] The value to enqueue
54
+ # @return [self]
55
+ #
56
+ # @example
57
+ # queue = AtomicQueue.new
58
+ # queue.push(1)
59
+ # queue.push("two")
60
+ # queue.push(nil)
61
+ # puts queue.size #=> 3
62
+ #
63
+ # @rbs (untyped value) -> self
64
+ def push(value)
65
+ _push(value)
66
+ end
67
+ # Alias for {#push}.
68
+ #
69
+ # @rbs (untyped value) -> self
70
+ alias << push
71
+
72
+ # Dequeues the value at the head of the queue, or returns nil when
73
+ # the queue is empty.
74
+ #
75
+ # This operation is atomic and thread-safe. Multiple threads may
76
+ # pop concurrently without blocking one another.
77
+ #
78
+ # @return [untyped, nil] The dequeued value, or nil when the queue is empty
79
+ #
80
+ # @example
81
+ # queue = AtomicQueue.new
82
+ # queue.push(1)
83
+ # queue.push(2)
84
+ # puts queue.pop #=> 1
85
+ # puts queue.pop #=> 2
86
+ # puts queue.pop #=> nil
87
+ #
88
+ # @rbs () -> untyped
89
+ def pop
90
+ _pop
91
+ end
92
+
93
+ # Returns the number of values currently queued.
94
+ #
95
+ # This operation is atomic and thread-safe. The returned value
96
+ # reflects the state at the time of the call, but may change
97
+ # immediately after in concurrent environments.
98
+ #
99
+ # @return [Integer] The number of queued values
100
+ #
101
+ # @example
102
+ # queue = AtomicQueue.new
103
+ # queue.push(1)
104
+ # queue.push(2)
105
+ # puts queue.size #=> 2
106
+ #
107
+ # @rbs () -> Integer
108
+ def size
109
+ _size
110
+ end
111
+ # Alias for {#size}.
112
+ #
113
+ # @rbs () -> Integer
114
+ alias length size
115
+
116
+ # Returns true when the queue is empty.
117
+ #
118
+ # This operation is atomic and thread-safe. The returned value
119
+ # reflects the state at the time of the call, but may change
120
+ # immediately after in concurrent environments.
121
+ #
122
+ # @return [true, false] true when the queue has no values
123
+ #
124
+ # @example
125
+ # queue = AtomicQueue.new
126
+ # puts queue.empty? #=> true
127
+ # queue.push(1)
128
+ # puts queue.empty? #=> false
129
+ #
130
+ # @rbs () -> bool
131
+ def empty?
132
+ _empty_p
133
+ end
134
+ end
135
+ end
136
+
137
+ AtomicQueue = AtomicRuby::AtomicQueue
@@ -2,18 +2,17 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative "atom"
5
+ require_relative "atomic_boolean"
6
+ require_relative "atomic_condition_variable"
7
+ require_relative "atomic_queue"
5
8
 
6
9
  module AtomicRuby
7
10
  # Provides a fixed-size thread pool using atomic operations for work queuing.
8
11
  #
9
12
  # 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
- # The queue is implemented as a two-stack structure (in/out) backed by
14
- # immutable frozen linked-list nodes. Enqueueing prepends to the `in` stack
15
- # in O(1); dequeueing pops from the `out` stack in O(1), reversing `in` into
16
- # `out` only when `out` is exhausted (amortized O(1) per item).
13
+ # work items from an {AtomicQueue}. Both enqueueing and dequeueing are O(1)
14
+ # and lock-free, so concurrent producers and consumers never block one
15
+ # another.
17
16
  #
18
17
  # @example Basic usage
19
18
  # pool = AtomicThreadPool.new(size: 4)
@@ -85,7 +84,9 @@ module AtomicRuby
85
84
  @name = name
86
85
  @on_error = on_error
87
86
 
88
- @state = Atom.new(in: nil, out: nil, count: 0, shutdown: false)
87
+ @queue = AtomicQueue.new
88
+ @shutdown = AtomicBoolean.new(false)
89
+ @work_available = AtomicConditionVariable.new
89
90
  @started_thread_count = Atom.new(0)
90
91
  @active_thread_count = Atom.new(0)
91
92
  @threads = []
@@ -117,17 +118,10 @@ module AtomicRuby
117
118
  #
118
119
  # @rbs (Proc work) -> void
119
120
  def <<(work)
120
- state = @state.swap do |current_state|
121
- if current_state[:shutdown]
122
- current_state
123
- else
124
- new_node = { value: work, next: current_state[:in] }.freeze
125
- current_state.merge(in: new_node, count: current_state[:count] + 1)
126
- end
127
- end
128
- raise EnqueuedWorkAfterShutdownError if state[:shutdown]
121
+ raise EnqueuedWorkAfterShutdownError if @shutdown.true?
129
122
 
130
- @threads.each(&:wakeup)
123
+ @queue.push(work)
124
+ @work_available.signal
131
125
  end
132
126
 
133
127
  # Returns the number of currently alive worker threads.
@@ -168,7 +162,7 @@ module AtomicRuby
168
162
  #
169
163
  # @rbs () -> Integer
170
164
  def queue_length
171
- @state.value[:count]
165
+ @queue.size
172
166
  end
173
167
  # Alias for {#queue_length}.
174
168
  #
@@ -225,17 +219,10 @@ module AtomicRuby
225
219
  #
226
220
  # @rbs () -> void
227
221
  def shutdown
228
- already_shutdown = false
229
- @state.swap do |current_state|
230
- if current_state[:shutdown]
231
- already_shutdown = true
232
- current_state
233
- else
234
- current_state.merge(shutdown: true)
235
- end
236
- end
237
- return if already_shutdown
222
+ return if @shutdown.true?
238
223
 
224
+ @shutdown.make_true
225
+ @work_available.broadcast
239
226
  @threads.each(&:join)
240
227
  end
241
228
 
@@ -263,40 +250,26 @@ module AtomicRuby
263
250
  work = nil
264
251
  should_shutdown = false
265
252
 
266
- @state.swap do |current_state|
267
- if current_state[:shutdown] && current_state[:in].nil? && current_state[:out].nil?
268
- should_shutdown = true
269
- current_state
270
- elsif current_state[:out]
271
- work = current_state[:out][:value]
272
- current_state.merge(out: current_state[:out][:next], count: current_state[:count] - 1)
273
- elsif current_state[:in]
274
- new_out = reverse_list(current_state[:in])
275
- work = new_out[:value]
276
- current_state.merge(in: nil, out: new_out[:next], count: current_state[:count] - 1)
277
- else
278
- current_state
279
- end
253
+ @work_available.wait do
254
+ work = @queue.pop
255
+ should_shutdown = @shutdown.true? && @queue.empty? unless work
256
+ work || should_shutdown
280
257
  end
281
258
 
282
- if should_shutdown
283
- break
284
- elsif work
285
- @active_thread_count.swap { |current_count| current_count + 1 }
286
- begin
287
- work.call
288
- rescue => err
289
- if @on_error
290
- @on_error.call(err)
291
- else
292
- warn "#{thread_name} rescued:"
293
- warn err.full_message
294
- end
295
- ensure
296
- @active_thread_count.swap { |current_count| current_count - 1 }
259
+ break if should_shutdown
260
+
261
+ @active_thread_count.swap { |current_count| current_count + 1 }
262
+ begin
263
+ work.call
264
+ rescue => err
265
+ if @on_error
266
+ @on_error.call(err)
267
+ else
268
+ warn "#{thread_name} rescued:"
269
+ warn err.full_message
297
270
  end
298
- else
299
- sleep 0.001
271
+ ensure
272
+ @active_thread_count.swap { |current_count| current_count - 1 }
300
273
  end
301
274
  end
302
275
  end
@@ -305,22 +278,6 @@ module AtomicRuby
305
278
 
306
279
  Thread.pass until @started_thread_count.value == @size
307
280
  end
308
-
309
- # Reverses a linked-list of frozen nodes, returning a new reversed list.
310
- # Each node in the returned list is a new frozen hash.
311
- #
312
- # @param node [Hash, nil] Head of the list to reverse
313
- # @return [Hash, nil] Head of the reversed list
314
- #
315
- # @rbs (Hash? node) -> Hash?
316
- def reverse_list(node)
317
- reversed = nil
318
- while node
319
- reversed = { value: node[:value], next: reversed }.freeze
320
- node = node[:next]
321
- end
322
- reversed
323
- end
324
281
  end
325
282
  end
326
283
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.12.0"
4
+ VERSION = "0.14.0"
5
5
  end
data/lib/atomic-ruby.rb CHANGED
@@ -3,5 +3,7 @@
3
3
  require_relative "atomic-ruby/version"
4
4
  require_relative "atomic-ruby/atom"
5
5
  require_relative "atomic-ruby/atomic_boolean"
6
+ require_relative "atomic-ruby/atomic_condition_variable"
7
+ require_relative "atomic-ruby/atomic_queue"
6
8
  require_relative "atomic-ruby/atomic_thread_pool"
7
9
  require_relative "atomic-ruby/atomic_count_down_latch"
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.12.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young
@@ -27,7 +27,9 @@ files:
27
27
  - lib/atomic-ruby.rb
28
28
  - lib/atomic-ruby/atom.rb
29
29
  - lib/atomic-ruby/atomic_boolean.rb
30
+ - lib/atomic-ruby/atomic_condition_variable.rb
30
31
  - lib/atomic-ruby/atomic_count_down_latch.rb
32
+ - lib/atomic-ruby/atomic_queue.rb
31
33
  - lib/atomic-ruby/atomic_thread_pool.rb
32
34
  - lib/atomic-ruby/version.rb
33
35
  homepage: https://github.com/joshuay03/atomic-ruby
@@ -50,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
52
  - !ruby/object:Gem::Version
51
53
  version: '0'
52
54
  requirements: []
53
- rubygems_version: 4.0.10
55
+ rubygems_version: 4.0.16
54
56
  specification_version: 4
55
57
  summary: Atomic primitives for Ruby
56
58
  test_files: []