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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +129 -30
- data/ext/atomic_ruby/atomic_ruby.c +422 -0
- data/lib/atomic-ruby/atomic_condition_variable.rb +20 -34
- data/lib/atomic-ruby/atomic_queue.rb +137 -0
- data/lib/atomic-ruby/atomic_thread_pool.rb +14 -59
- data/lib/atomic-ruby/version.rb +1 -1
- data/lib/atomic-ruby.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5a0461b3ac6fd6dbc7674cf028f8fb3727cad76da32ad2c6054a92a2818fe25f
|
|
4
|
+
data.tar.gz: 6f20eabbb25f29f6c43c166cbe240c1ddf2a6def437317c392a53fe86699a6b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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`
|
|
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.
|
|
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.
|
|
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.
|
|
245
|
-
Concurrent Ruby Atomic Bank Account: 5.
|
|
246
|
-
Atomic Ruby Atomic Bank Account: 5.
|
|
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.
|
|
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.
|
|
350
|
+
atomic-ruby version: 0.14.0
|
|
328
351
|
|
|
329
352
|
Warming up --------------------------------------
|
|
330
|
-
Synchronized Boolean Toggle
|
|
331
|
-
Concurrent Ruby Atomic Boolean Toggle
|
|
332
|
-
Atomic Ruby Atomic Boolean Toggle
|
|
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.
|
|
335
|
-
Concurrent Ruby Atomic Boolean Toggle 1.
|
|
336
|
-
Atomic Ruby Atomic Boolean Toggle 1.
|
|
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
|
|
337
360
|
|
|
338
361
|
Comparison:
|
|
339
|
-
Synchronized Boolean Toggle:
|
|
340
|
-
Concurrent Ruby Atomic Boolean Toggle:
|
|
341
|
-
Atomic Ruby Atomic Boolean Toggle:
|
|
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.
|
|
410
|
-
atomic-ruby version: 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
|
|
414
|
-
Atomic Ruby Atomic
|
|
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
|
|
417
|
-
Atomic Ruby Atomic
|
|
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
|
|
418
516
|
|
|
419
517
|
Comparison:
|
|
420
|
-
Synchronized
|
|
421
|
-
Atomic Ruby Atomic
|
|
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.
|
|
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.
|
|
578
|
+
atomic-ruby version: 0.14.0
|
|
480
579
|
|
|
481
580
|
Benchmark Results:
|
|
482
|
-
Concurrent Ruby Thread Pool: 5.
|
|
483
|
-
Atomic Ruby Atomic Thread Pool:
|
|
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
|
-
|
|
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.
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# `Thread.stop` and
|
|
15
|
-
#
|
|
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 =
|
|
39
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
143
|
+
waiter = _add_waiter(self_thread)
|
|
158
144
|
result = yield
|
|
159
145
|
if result
|
|
160
|
-
|
|
146
|
+
_remove_waiter(waiter)
|
|
161
147
|
return result
|
|
162
148
|
end
|
|
163
149
|
|
|
164
150
|
Thread.stop
|
|
165
|
-
|
|
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
|
|
12
|
-
#
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
@
|
|
271
|
-
|
|
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
|
|
data/lib/atomic-ruby/version.rb
CHANGED
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.
|
|
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.
|
|
55
|
+
rubygems_version: 4.0.16
|
|
55
56
|
specification_version: 4
|
|
56
57
|
summary: Atomic primitives for Ruby
|
|
57
58
|
test_files: []
|