atomic-ruby 0.13.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: 540189a4554f5e046af0b5305063a96be9fa58cfac1740e5e6076d6b1c48cd40
4
- data.tar.gz: 528fa3c2e94c0d403ec8792076a773d3e7ce800852f1472dbde8e9ce7bbfb6f8
3
+ metadata.gz: 5a0461b3ac6fd6dbc7674cf028f8fb3727cad76da32ad2c6054a92a2818fe25f
4
+ data.tar.gz: 6f20eabbb25f29f6c43c166cbe240c1ddf2a6def437317c392a53fe86699a6b8
5
5
  SHA512:
6
- metadata.gz: 8ad39592c8615f88aa0da987d1de06d87496c6c82481a9678c907937de65d308f4595e54008b0e357d99605fc660d8b35b986397ad0573e4b81dbf5de8926695
7
- data.tar.gz: 46794d17727ab6b835210c7fc8e901d8c7daf37ff319c16b0efdb87cdc8bd9c95b6fb2dad171f96ca8d7b52b3fb8bd08cfde002f264cbe2155e6041310e74827
6
+ metadata.gz: 8e29e25f0790c2c3f3417efae11440a5f0500e0e943ce4c4a903f7fc4180342e832319cfe82712284449c876c70aa29ea22899480668bb1f35cc02c9369f8a6c
7
+ data.tar.gz: f26bc0f694a0e00cc84c9988761a1cf56c50c89d499bd70eb88b3fb896749ba36465d53d16e03d5ad636c8e8cd59b5b1f179bca827cd85c9a59397951bcd6013
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## [0.13.0] - 2026-06-12
4
10
 
5
11
  - Replace `sleep` with `AtomicConditionVariable` in idle `AtomicThreadPool` workers
data/README.md CHANGED
@@ -65,11 +65,32 @@ Thread.pass until condvar.waiter_count == 1
65
65
  p condvar.waiter_count #=> 1
66
66
 
67
67
  ready.make_true
68
- p condvar.signal #=> true
68
+ p condvar.signal #=> true
69
69
  waiter.join
70
70
  p condvar.waiter_count #=> 0
71
71
  ```
72
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
+
73
94
  `AtomicThreadPool`:
74
95
 
75
96
  ```ruby
@@ -119,7 +140,9 @@ p latch.count #=> 0
119
140
  ```
120
141
 
121
142
  > [!NOTE]
122
- > `Atom`, `AtomicBoolean`, and `AtomicCountDownLatch` are Ractor-safe in Ruby 4.0+. `AtomicConditionVariable` is not, since it parks `Thread` references which cannot be shared across ractors.
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.
123
146
 
124
147
  ## Benchmarks
125
148
 
@@ -231,9 +254,9 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
231
254
  ```
232
255
  > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
233
256
 
234
- ruby version: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
257
+ ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
235
258
  concurrent-ruby version: 1.3.6
236
- atomic-ruby version: 0.13.0
259
+ atomic-ruby version: 0.14.0
237
260
 
238
261
  Balances:
239
262
  Synchronized Bank Account Balance: 975
@@ -241,9 +264,9 @@ Concurrent Ruby Atomic Bank Account Balance: 975
241
264
  Atomic Ruby Atomic Bank Account Balance: 975
242
265
 
243
266
  Benchmark Results:
244
- Synchronized Bank Account: 5.109724 seconds
245
- Concurrent Ruby Atomic Bank Account: 5.137749 seconds
246
- Atomic Ruby Atomic Bank Account: 5.101637 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
247
270
  ```
248
271
 
249
272
  </details>
@@ -322,23 +345,23 @@ end
322
345
  ```
323
346
  > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
324
347
 
325
- ruby version: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
348
+ ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
326
349
  concurrent-ruby version: 1.3.6
327
- atomic-ruby version: 0.13.0
350
+ atomic-ruby version: 0.14.0
328
351
 
329
352
  Warming up --------------------------------------
330
- Synchronized Boolean Toggle 167.000 i/100ms
331
- Concurrent Ruby Atomic Boolean Toggle 132.000 i/100ms
332
- Atomic Ruby Atomic Boolean Toggle 117.000 i/100ms
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
333
356
  Calculating -------------------------------------
334
- Synchronized Boolean Toggle 1.653k (± 2.0%) i/s (605.04 μs/i) - 8.350k in 5.052096s
335
- Concurrent Ruby Atomic Boolean Toggle 1.313k (± 2.1%) i/s (761.45 μs/i) - 6.600k in 5.025579s
336
- Atomic Ruby Atomic Boolean Toggle 1.230k4.8%) i/s (812.86 μs/i) - 6.201k in 5.040557s
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.190k2.6%) i/s (840.11 μs/i) - 6.000k in 5.040665s
337
360
 
338
361
  Comparison:
339
- Synchronized Boolean Toggle: 1652.8 i/s
340
- Concurrent Ruby Atomic Boolean Toggle: 1313.3 i/s - 1.26x slower
341
- Atomic Ruby Atomic Boolean Toggle: 1230.2 i/s - 1.34x slower
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
342
365
  ```
343
366
 
344
367
  </details>
@@ -406,19 +429,95 @@ end
406
429
  ```
407
430
  > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_condition_variable_benchmark.rb
408
431
 
409
- ruby version: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
410
- atomic-ruby version: 0.13.0
432
+ ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
433
+ atomic-ruby version: 0.14.0
434
+
435
+ Warming up --------------------------------------
436
+ Synchronized Condition Variable Wait/Signal 3.977k i/100ms
437
+ Atomic Ruby Atomic Condition Variable Wait/Signal 3.885k i/100ms
438
+ Calculating -------------------------------------
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.046k (± 4.2%) i/s (25.61 μs/i) - 198.135k in 5.074411s
441
+
442
+ Comparison:
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
411
509
 
412
510
  Warming up --------------------------------------
413
- Synchronized Condition Variable Wait/Signal 3.847k i/100ms
414
- Atomic Ruby Atomic Condition Variable Wait/Signal 3.704k i/100ms
511
+ Synchronized Queue Push/Pop 181.000 i/100ms
512
+ Atomic Ruby Atomic Queue Push/Pop 132.000 i/100ms
415
513
  Calculating -------------------------------------
416
- Synchronized Condition Variable Wait/Signal 38.661k4.4%) i/s (25.87 μs/i) - 196.197k in 5.074793s
417
- Atomic Ruby Atomic Condition Variable Wait/Signal 37.895k4.7%) i/s (26.39 μs/i) - 192.608k in 5.082648s
514
+ Synchronized Queue Push/Pop 1.845k2.1%) i/s (541.91 μs/i) - 9.231k in 5.002394s
515
+ Atomic Ruby Atomic Queue Push/Pop 1.431k6.8%) i/s (698.64 μs/i) - 7.260k in 5.072143s
418
516
 
419
517
  Comparison:
420
- Synchronized Condition Variable Wait/Signal: 38661.1 i/s
421
- Atomic Ruby Atomic Condition Variable Wait/Signal: 37895.2 i/s - same-ish: difference falls within error
518
+ Synchronized Queue Push/Pop: 1845.3 i/s
519
+ Atomic Ruby Atomic Queue Push/Pop: 1431.3 i/s - 1.29x slower
520
+
422
521
  ```
423
522
 
424
523
  </details>
@@ -474,13 +573,13 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
474
573
  ```
475
574
  > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
476
575
 
477
- ruby version: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
576
+ ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
478
577
  concurrent-ruby version: 1.3.6
479
- atomic-ruby version: 0.13.0
578
+ atomic-ruby version: 0.14.0
480
579
 
481
580
  Benchmark Results:
482
- Concurrent Ruby Thread Pool: 5.792203 seconds
483
- Atomic Ruby Atomic Thread Pool: 5.504388 seconds
581
+ Concurrent Ruby Thread Pool: 5.078227 seconds
582
+ Atomic Ruby Atomic Thread Pool: 4.824681 seconds
484
583
  ```
485
584
 
486
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
  }
@@ -1,18 +1,19 @@
1
1
  # rbs_inline: enabled
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "atom"
4
+ require "atomic_ruby/atomic_ruby"
5
5
 
6
6
  module AtomicRuby
7
7
  # Provides lock-free wait/signal coordination using atomic operations.
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. Coordination is done by publishing the
12
- # set of parked threads through an {Atom}, so all in-process
13
- # synchronisation stays on the gem's CAS path. Parking still uses
14
- # `Thread.stop` and `Thread#wakeup`, which are the standard kernel-level
15
- # primitives Ruby exposes for sleeping a thread.
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.
16
17
  #
17
18
  # The lost-wakeup race in a naive `check-then-park` consumer is avoided
18
19
  # by the {#wait} contract: a waiter registers itself before re-evaluating
@@ -35,11 +36,8 @@ module AtomicRuby
35
36
  #
36
37
  # @example Worker loop draining an atomic queue
37
38
  # condvar.wait do
38
- # work = nil
39
- # queue.swap do |q|
40
- # q.empty? ? q : (work = q.first; q.drop(1).freeze)
41
- # end
42
- # work
39
+ # work = queue.pop
40
+ # work || shutdown.true?
43
41
  # end
44
42
  #
45
43
  # @note This class is NOT Ractor-safe as it parks `Thread` references,
@@ -52,7 +50,7 @@ module AtomicRuby
52
50
  #
53
51
  # @rbs () -> void
54
52
  def initialize
55
- @waiters = Atom.new([].freeze)
53
+ _initialize
56
54
  end
57
55
 
58
56
  # Returns the number of currently parked waiters.
@@ -69,7 +67,7 @@ module AtomicRuby
69
67
  #
70
68
  # @rbs () -> Integer
71
69
  def waiter_count
72
- @waiters.value.size
70
+ _waiter_count
73
71
  end
74
72
 
75
73
  # Wakes one parked waiter, or no-ops if none are parked.
@@ -86,18 +84,10 @@ module AtomicRuby
86
84
  #
87
85
  # @rbs () -> bool
88
86
  def signal
89
- target = nil
90
- @waiters.swap do |waiters|
91
- if waiters.empty?
92
- waiters
93
- else
94
- target = waiters.first
95
- waiters.drop(1).freeze
96
- end
97
- end
98
- return false unless target
87
+ thread = _shift_thread
88
+ return false unless thread
99
89
 
100
- target.wakeup rescue nil
90
+ thread.wakeup rescue nil
101
91
  true
102
92
  end
103
93
 
@@ -113,13 +103,9 @@ module AtomicRuby
113
103
  #
114
104
  # @rbs () -> Integer
115
105
  def broadcast
116
- targets = nil
117
- @waiters.swap do |waiters|
118
- targets = waiters
119
- [].freeze
120
- end
121
- targets.each { |thread| thread.wakeup rescue nil }
122
- targets.size
106
+ threads = _drain_threads
107
+ threads.each { |thread| thread.wakeup rescue nil }
108
+ threads.size
123
109
  end
124
110
 
125
111
  # Blocks until the given block returns a truthy value, then returns that
@@ -154,15 +140,15 @@ module AtomicRuby
154
140
 
155
141
  self_thread = Thread.current
156
142
  loop do
157
- @waiters.swap { |waiters| (waiters + [self_thread]).freeze }
143
+ waiter = _add_waiter(self_thread)
158
144
  result = yield
159
145
  if result
160
- @waiters.swap { |waiters| (waiters - [self_thread]).freeze }
146
+ _remove_waiter(waiter)
161
147
  return result
162
148
  end
163
149
 
164
150
  Thread.stop
165
- @waiters.swap { |waiters| (waiters - [self_thread]).freeze }
151
+ _remove_waiter(waiter)
166
152
  end
167
153
  end
168
154
  end
@@ -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,19 +2,17 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative "atom"
5
+ require_relative "atomic_boolean"
5
6
  require_relative "atomic_condition_variable"
7
+ require_relative "atomic_queue"
6
8
 
7
9
  module AtomicRuby
8
10
  # Provides a fixed-size thread pool using atomic operations for work queuing.
9
11
  #
10
12
  # AtomicThreadPool maintains a fixed number of worker threads that process
11
- # work items from an atomic queue. The pool uses compare-and-swap operations
12
- # for thread-safe work enqueueing and state management.
13
- #
14
- # The queue is implemented as a two-stack structure (in/out) backed by
15
- # immutable frozen linked-list nodes. Enqueueing prepends to the `in` stack
16
- # in O(1); dequeueing pops from the `out` stack in O(1), reversing `in` into
17
- # `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.
18
16
  #
19
17
  # @example Basic usage
20
18
  # pool = AtomicThreadPool.new(size: 4)
@@ -86,7 +84,8 @@ module AtomicRuby
86
84
  @name = name
87
85
  @on_error = on_error
88
86
 
89
- @state = Atom.new(in: nil, out: nil, count: 0, shutdown: false)
87
+ @queue = AtomicQueue.new
88
+ @shutdown = AtomicBoolean.new(false)
90
89
  @work_available = AtomicConditionVariable.new
91
90
  @started_thread_count = Atom.new(0)
92
91
  @active_thread_count = Atom.new(0)
@@ -119,16 +118,9 @@ module AtomicRuby
119
118
  #
120
119
  # @rbs (Proc work) -> void
121
120
  def <<(work)
122
- state = @state.swap do |current_state|
123
- if current_state[:shutdown]
124
- current_state
125
- else
126
- new_node = { value: work, next: current_state[:in] }.freeze
127
- current_state.merge(in: new_node, count: current_state[:count] + 1)
128
- end
129
- end
130
- raise EnqueuedWorkAfterShutdownError if state[:shutdown]
121
+ raise EnqueuedWorkAfterShutdownError if @shutdown.true?
131
122
 
123
+ @queue.push(work)
132
124
  @work_available.signal
133
125
  end
134
126
 
@@ -170,7 +162,7 @@ module AtomicRuby
170
162
  #
171
163
  # @rbs () -> Integer
172
164
  def queue_length
173
- @state.value[:count]
165
+ @queue.size
174
166
  end
175
167
  # Alias for {#queue_length}.
176
168
  #
@@ -227,17 +219,9 @@ module AtomicRuby
227
219
  #
228
220
  # @rbs () -> void
229
221
  def shutdown
230
- already_shutdown = false
231
- @state.swap do |current_state|
232
- if current_state[:shutdown]
233
- already_shutdown = true
234
- current_state
235
- else
236
- current_state.merge(shutdown: true)
237
- end
238
- end
239
- return if already_shutdown
222
+ return if @shutdown.true?
240
223
 
224
+ @shutdown.make_true
241
225
  @work_available.broadcast
242
226
  @threads.each(&:join)
243
227
  end
@@ -267,21 +251,8 @@ module AtomicRuby
267
251
  should_shutdown = false
268
252
 
269
253
  @work_available.wait do
270
- @state.swap do |current_state|
271
- if current_state[:shutdown] && current_state[:in].nil? && current_state[:out].nil?
272
- should_shutdown = true
273
- current_state
274
- elsif current_state[:out]
275
- work = current_state[:out][:value]
276
- current_state.merge(out: current_state[:out][:next], count: current_state[:count] - 1)
277
- elsif current_state[:in]
278
- new_out = reverse_list(current_state[:in])
279
- work = new_out[:value]
280
- current_state.merge(in: nil, out: new_out[:next], count: current_state[:count] - 1)
281
- else
282
- current_state
283
- end
284
- end
254
+ work = @queue.pop
255
+ should_shutdown = @shutdown.true? && @queue.empty? unless work
285
256
  work || should_shutdown
286
257
  end
287
258
 
@@ -307,22 +278,6 @@ module AtomicRuby
307
278
 
308
279
  Thread.pass until @started_thread_count.value == @size
309
280
  end
310
-
311
- # Reverses a linked-list of frozen nodes, returning a new reversed list.
312
- # Each node in the returned list is a new frozen hash.
313
- #
314
- # @param node [Hash, nil] Head of the list to reverse
315
- # @return [Hash, nil] Head of the reversed list
316
- #
317
- # @rbs (Hash? node) -> Hash?
318
- def reverse_list(node)
319
- reversed = nil
320
- while node
321
- reversed = { value: node[:value], next: reversed }.freeze
322
- node = node[:next]
323
- end
324
- reversed
325
- end
326
281
  end
327
282
  end
328
283
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.13.0"
4
+ VERSION = "0.14.0"
5
5
  end
data/lib/atomic-ruby.rb CHANGED
@@ -4,5 +4,6 @@ require_relative "atomic-ruby/version"
4
4
  require_relative "atomic-ruby/atom"
5
5
  require_relative "atomic-ruby/atomic_boolean"
6
6
  require_relative "atomic-ruby/atomic_condition_variable"
7
+ require_relative "atomic-ruby/atomic_queue"
7
8
  require_relative "atomic-ruby/atomic_thread_pool"
8
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.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young
@@ -29,6 +29,7 @@ files:
29
29
  - lib/atomic-ruby/atomic_boolean.rb
30
30
  - lib/atomic-ruby/atomic_condition_variable.rb
31
31
  - lib/atomic-ruby/atomic_count_down_latch.rb
32
+ - lib/atomic-ruby/atomic_queue.rb
32
33
  - lib/atomic-ruby/atomic_thread_pool.rb
33
34
  - lib/atomic-ruby/version.rb
34
35
  homepage: https://github.com/joshuay03/atomic-ruby
@@ -51,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
52
  - !ruby/object:Gem::Version
52
53
  version: '0'
53
54
  requirements: []
54
- rubygems_version: 4.0.10
55
+ rubygems_version: 4.0.16
55
56
  specification_version: 4
56
57
  summary: Atomic primitives for Ruby
57
58
  test_files: []