atomic-ruby 0.8.1 → 0.9.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 +20 -31
- data/ext/atomic_ruby/atomic_ruby.c +49 -12
- data/ext/atomic_ruby/atomic_ruby.h +1 -1
- data/ext/atomic_ruby/extconf.rb +0 -3
- data/lib/atomic-ruby/atom.rb +5 -6
- data/lib/atomic-ruby/atomic_thread_pool.rb +16 -9
- data/lib/atomic-ruby/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6bef30ee19f89bf213e837f5ee565b4e0b2f926c422dd7204270f349a331142c
|
|
4
|
+
data.tar.gz: b19ac4ac6c8f363c891aae5c7ef4b4c39c80b420673f71890fe8c4f1974dee89
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8afd84e8e6cc724949c88c6485bd51e8d3ba86e56852ad742136613ce2f4a36468de88b7c011a2061218ff7edb189a8e8a13f3049741d5a020862c8cc9760e7d
|
|
7
|
+
data.tar.gz: d5727d75e5b3c6f4cce6c7dd394b1fd7d64d48f3940124a27f2725e1947c6aac375604fb98364a24aa6bbcb50670680b3b755c566f5092829b21eff7bef98d10
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.9.0] - 2025-11-05
|
|
4
|
+
|
|
5
|
+
- Switch `AtomicThreadPool` back to atomics now that Ractor safety is lazy
|
|
6
|
+
- Don't enforce Ractor safety unless crossing Ractor boundaries
|
|
7
|
+
- Add `AtomicThreadPool#size` and `AtomicThreadPool#queue_size` aliases
|
|
8
|
+
|
|
3
9
|
## [0.8.1] - 2025-11-01
|
|
4
10
|
|
|
5
11
|
- Don't require `AtomicThreadPool#<<` to be given a shareable proc
|
data/README.md
CHANGED
|
@@ -99,17 +99,6 @@ p latch.count #=> 0
|
|
|
99
99
|
|
|
100
100
|
> [!NOTE]
|
|
101
101
|
> `Atom`, `AtomicBoolean`, and `AtomicCountDownLatch` are Ractor-safe in Ruby 3.5+.
|
|
102
|
-
>
|
|
103
|
-
> When storing procs in atoms, create them using `Ractor.shareable_proc`, as `Ractor.make_shareable`
|
|
104
|
-
> cannot convert regular procs to shareable ones when the proc's context is not shareable.
|
|
105
|
-
>
|
|
106
|
-
> ```ruby
|
|
107
|
-
> # This will raise an error in Ruby 3.5+ (proc created in non-shareable context)
|
|
108
|
-
> atom = Atom.new(proc { puts "hello" })
|
|
109
|
-
>
|
|
110
|
-
> # Use this instead
|
|
111
|
-
> atom = Atom.new(Ractor.shareable_proc { puts "hello" })
|
|
112
|
-
> ```
|
|
113
102
|
|
|
114
103
|
## Benchmarks
|
|
115
104
|
|
|
@@ -221,9 +210,9 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
|
|
|
221
210
|
```
|
|
222
211
|
> bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
|
|
223
212
|
|
|
224
|
-
ruby version: ruby 3.5.0dev (2025-
|
|
213
|
+
ruby version: ruby 3.5.0dev (2025-11-05T10:35:48Z master 946d2d036f) +YJIT +PRISM [arm64-darwin25]
|
|
225
214
|
concurrent-ruby version: 1.3.5
|
|
226
|
-
atomic-ruby version: 0.
|
|
215
|
+
atomic-ruby version: 0.9.0
|
|
227
216
|
|
|
228
217
|
Balances:
|
|
229
218
|
Synchronized Bank Account Balance: 975
|
|
@@ -231,9 +220,9 @@ Concurrent Ruby Atomic Bank Account Balance: 975
|
|
|
231
220
|
Atomic Ruby Atomic Bank Account Balance: 975
|
|
232
221
|
|
|
233
222
|
Benchmark Results:
|
|
234
|
-
Synchronized Bank Account: 5.
|
|
235
|
-
Concurrent Ruby Atomic Bank Account: 5.
|
|
236
|
-
Atomic Ruby Atomic Bank Account: 5.
|
|
223
|
+
Synchronized Bank Account: 5.104234 seconds
|
|
224
|
+
Concurrent Ruby Atomic Bank Account: 5.113334 seconds
|
|
225
|
+
Atomic Ruby Atomic Bank Account: 5.097197 seconds
|
|
237
226
|
```
|
|
238
227
|
|
|
239
228
|
</details>
|
|
@@ -312,29 +301,29 @@ end
|
|
|
312
301
|
```
|
|
313
302
|
> bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
|
|
314
303
|
|
|
315
|
-
ruby version: ruby 3.5.0dev (2025-
|
|
304
|
+
ruby version: ruby 3.5.0dev (2025-11-05T10:35:48Z master 946d2d036f) +YJIT +PRISM [arm64-darwin25]
|
|
316
305
|
concurrent-ruby version: 1.3.5
|
|
317
|
-
atomic-ruby version: 0.
|
|
306
|
+
atomic-ruby version: 0.9.0
|
|
318
307
|
|
|
319
308
|
Warming up --------------------------------------
|
|
320
309
|
Synchronized Boolean Toggle
|
|
321
|
-
|
|
310
|
+
158.000 i/100ms
|
|
322
311
|
Concurrent Ruby Atomic Boolean Toggle
|
|
323
|
-
|
|
312
|
+
113.000 i/100ms
|
|
324
313
|
Atomic Ruby Atomic Boolean Toggle
|
|
325
|
-
|
|
314
|
+
122.000 i/100ms
|
|
326
315
|
Calculating -------------------------------------
|
|
327
316
|
Synchronized Boolean Toggle
|
|
328
|
-
1.
|
|
317
|
+
1.521k (± 2.1%) i/s (657.49 μs/i) - 7.742k in 5.092579s
|
|
329
318
|
Concurrent Ruby Atomic Boolean Toggle
|
|
330
|
-
1.
|
|
319
|
+
1.141k (± 1.6%) i/s (876.12 μs/i) - 5.763k in 5.050298s
|
|
331
320
|
Atomic Ruby Atomic Boolean Toggle
|
|
332
|
-
1.
|
|
321
|
+
1.243k (± 1.3%) i/s (804.64 μs/i) - 6.222k in 5.007246s
|
|
333
322
|
|
|
334
323
|
Comparison:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
Concurrent Ruby Atomic Boolean Toggle:
|
|
324
|
+
Synchronized Boolean Toggle: 1520.9 i/s
|
|
325
|
+
Atomic Ruby Atomic Boolean Toggle: 1242.8 i/s - 1.22x slower
|
|
326
|
+
Concurrent Ruby Atomic Boolean Toggle: 1141.4 i/s - 1.33x slower
|
|
338
327
|
```
|
|
339
328
|
|
|
340
329
|
</details>
|
|
@@ -390,13 +379,13 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
|
|
|
390
379
|
```
|
|
391
380
|
> bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
|
|
392
381
|
|
|
393
|
-
ruby version: ruby 3.5.0dev (2025-
|
|
382
|
+
ruby version: ruby 3.5.0dev (2025-11-05T10:35:48Z master 946d2d036f) +YJIT +PRISM [arm64-darwin25]
|
|
394
383
|
concurrent-ruby version: 1.3.5
|
|
395
|
-
atomic-ruby version: 0.
|
|
384
|
+
atomic-ruby version: 0.9.0
|
|
396
385
|
|
|
397
386
|
Benchmark Results:
|
|
398
|
-
Concurrent Ruby Thread Pool: 5.
|
|
399
|
-
Atomic Ruby Atomic Thread Pool: 4.
|
|
387
|
+
Concurrent Ruby Thread Pool: 5.169928 seconds
|
|
388
|
+
Atomic Ruby Atomic Thread Pool: 4.831942 seconds
|
|
400
389
|
```
|
|
401
390
|
|
|
402
391
|
</details>
|
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
typedef struct {
|
|
4
4
|
volatile VALUE value;
|
|
5
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
6
|
+
VALUE initialized_ractor;
|
|
7
|
+
#endif
|
|
5
8
|
} atomic_ruby_atom_t;
|
|
6
9
|
|
|
7
10
|
static void atomic_ruby_atom_mark(void *ptr) {
|
|
8
11
|
atomic_ruby_atom_t *atomic_ruby_atom = (atomic_ruby_atom_t *)ptr;
|
|
9
12
|
rb_gc_mark_movable(atomic_ruby_atom->value);
|
|
13
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
14
|
+
rb_gc_mark_movable(atomic_ruby_atom->initialized_ractor);
|
|
15
|
+
#endif
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
static void atomic_ruby_atom_free(void *ptr) {
|
|
@@ -21,6 +27,9 @@ static size_t atomic_ruby_atom_memsize(const void *ptr) {
|
|
|
21
27
|
static void atomic_ruby_atom_compact(void *ptr) {
|
|
22
28
|
atomic_ruby_atom_t *atomic_ruby_atom = (atomic_ruby_atom_t *)ptr;
|
|
23
29
|
atomic_ruby_atom->value = rb_gc_location(atomic_ruby_atom->value);
|
|
30
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
31
|
+
atomic_ruby_atom->initialized_ractor = rb_gc_location(atomic_ruby_atom->initialized_ractor);
|
|
32
|
+
#endif
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
static const rb_data_type_t atomic_ruby_atom_type = {
|
|
@@ -38,35 +47,54 @@ static const rb_data_type_t atomic_ruby_atom_type = {
|
|
|
38
47
|
#endif
|
|
39
48
|
};
|
|
40
49
|
|
|
50
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
51
|
+
static void ensure_value_shareable(VALUE self, atomic_ruby_atom_t *atom, VALUE value) {
|
|
52
|
+
bool check_shareable = NIL_P(atom->initialized_ractor);
|
|
53
|
+
|
|
54
|
+
if (!check_shareable) {
|
|
55
|
+
VALUE current_ractor = rb_funcall(rb_cRactor, rb_intern("current"), 0);
|
|
56
|
+
if (current_ractor != atom->initialized_ractor) {
|
|
57
|
+
check_shareable = true;
|
|
58
|
+
RB_OBJ_WRITE(self, &atom->initialized_ractor, Qnil);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (check_shareable && !rb_ractor_shareable_p(value)) {
|
|
63
|
+
rb_raise(rb_eArgError, "value must be a shareable object when used across ractors");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
#endif
|
|
67
|
+
|
|
41
68
|
static VALUE rb_cAtom_allocate(VALUE klass) {
|
|
42
69
|
atomic_ruby_atom_t *atomic_ruby_atom;
|
|
43
70
|
VALUE obj = TypedData_Make_Struct(klass, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
|
|
44
71
|
RB_OBJ_WRITE(obj, &atomic_ruby_atom->value, Qnil);
|
|
45
|
-
return obj;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
72
|
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
rb_raise(rb_eArgError, "value must be a shareable object");
|
|
52
|
-
}
|
|
53
|
-
}
|
|
73
|
+
VALUE current_ractor = rb_funcall(rb_cRactor, rb_intern("current"), 0);
|
|
74
|
+
RB_OBJ_WRITE(obj, &atomic_ruby_atom->initialized_ractor, current_ractor);
|
|
54
75
|
#endif
|
|
76
|
+
return obj;
|
|
77
|
+
}
|
|
55
78
|
|
|
56
79
|
static VALUE rb_cAtom_initialize(VALUE self, VALUE value) {
|
|
57
80
|
atomic_ruby_atom_t *atomic_ruby_atom;
|
|
58
81
|
TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
|
|
82
|
+
RB_OBJ_WRITE(self, &atomic_ruby_atom->value, value);
|
|
59
83
|
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
60
|
-
|
|
84
|
+
rb_obj_freeze(self);
|
|
85
|
+
FL_SET_RAW(self, RUBY_FL_SHAREABLE);
|
|
61
86
|
#endif
|
|
62
|
-
RB_OBJ_WRITE(self, &atomic_ruby_atom->value, value);
|
|
63
87
|
return self;
|
|
64
88
|
}
|
|
65
89
|
|
|
66
90
|
static VALUE rb_cAtom_value(VALUE self) {
|
|
67
91
|
atomic_ruby_atom_t *atomic_ruby_atom;
|
|
68
92
|
TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
|
|
69
|
-
|
|
93
|
+
VALUE value = (VALUE)RUBY_ATOMIC_PTR_LOAD(atomic_ruby_atom->value);
|
|
94
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
95
|
+
ensure_value_shareable(self, atomic_ruby_atom, value);
|
|
96
|
+
#endif
|
|
97
|
+
return value;
|
|
70
98
|
}
|
|
71
99
|
|
|
72
100
|
static VALUE rb_cAtom_swap(VALUE self) {
|
|
@@ -78,7 +106,7 @@ static VALUE rb_cAtom_swap(VALUE self) {
|
|
|
78
106
|
expected_old_value = atomic_ruby_atom->value;
|
|
79
107
|
new_value = rb_yield(expected_old_value);
|
|
80
108
|
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
81
|
-
|
|
109
|
+
ensure_value_shareable(self, atomic_ruby_atom, new_value);
|
|
82
110
|
#endif
|
|
83
111
|
} while (RUBY_ATOMIC_VALUE_CAS(atomic_ruby_atom->value, expected_old_value, new_value) != expected_old_value);
|
|
84
112
|
RB_OBJ_WRITTEN(self, expected_old_value, new_value);
|
|
@@ -86,6 +114,14 @@ static VALUE rb_cAtom_swap(VALUE self) {
|
|
|
86
114
|
return new_value;
|
|
87
115
|
}
|
|
88
116
|
|
|
117
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
118
|
+
static VALUE rb_cAtom_initialized_ractor(VALUE self) {
|
|
119
|
+
atomic_ruby_atom_t *atomic_ruby_atom;
|
|
120
|
+
TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
|
|
121
|
+
return atomic_ruby_atom->initialized_ractor;
|
|
122
|
+
}
|
|
123
|
+
#endif
|
|
124
|
+
|
|
89
125
|
RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
|
|
90
126
|
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
91
127
|
rb_ext_ractor_safe(true);
|
|
@@ -100,6 +136,7 @@ RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
|
|
|
100
136
|
rb_define_method(rb_cAtom, "_swap", rb_cAtom_swap, 0);
|
|
101
137
|
|
|
102
138
|
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
139
|
+
rb_define_method(rb_cAtom, "_initialized_ractor", rb_cAtom_initialized_ractor, 0);
|
|
103
140
|
rb_define_const(rb_mAtomicRuby, "RACTOR_SAFE", Qtrue);
|
|
104
141
|
#else
|
|
105
142
|
rb_define_const(rb_mAtomicRuby, "RACTOR_SAFE", Qfalse);
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
#include "ruby.h"
|
|
5
5
|
#include "ruby/atomic.h"
|
|
6
|
-
#include "ruby/ractor.h"
|
|
7
6
|
#include "ruby/version.h"
|
|
8
7
|
|
|
9
8
|
#if RUBY_API_VERSION_CODE >= 30500
|
|
10
9
|
#define ATOMIC_RUBY_RACTOR_SAFE 1
|
|
10
|
+
#include "ruby/ractor.h"
|
|
11
11
|
#endif
|
|
12
12
|
|
|
13
13
|
#endif /* ATOMIC_RUBY_ATOM_H */
|
data/ext/atomic_ruby/extconf.rb
CHANGED
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "mkmf"
|
|
4
4
|
|
|
5
|
-
# Makes all symbols private by default to avoid unintended conflict
|
|
6
|
-
# with other gems. To explicitly export symbols you can use RUBY_FUNC_EXPORTED
|
|
7
|
-
# selectively, or entirely remove this flag.
|
|
8
5
|
append_cflags("-fvisibility=hidden")
|
|
9
6
|
|
|
10
7
|
create_makefile("atomic_ruby/atomic_ruby")
|
data/lib/atomic-ruby/atom.rb
CHANGED
|
@@ -5,9 +5,7 @@ require "atomic_ruby/atomic_ruby"
|
|
|
5
5
|
module AtomicRuby
|
|
6
6
|
class Atom
|
|
7
7
|
def initialize(value)
|
|
8
|
-
_initialize(
|
|
9
|
-
|
|
10
|
-
freeze if RACTOR_SAFE
|
|
8
|
+
_initialize(value)
|
|
11
9
|
end
|
|
12
10
|
|
|
13
11
|
def value
|
|
@@ -16,14 +14,15 @@ module AtomicRuby
|
|
|
16
14
|
|
|
17
15
|
def swap(&block)
|
|
18
16
|
_swap do |old_value|
|
|
19
|
-
|
|
17
|
+
make_shareable_if_needed(block.call(old_value))
|
|
20
18
|
end
|
|
21
19
|
end
|
|
22
20
|
|
|
23
21
|
private
|
|
24
22
|
|
|
25
|
-
def
|
|
26
|
-
if RACTOR_SAFE
|
|
23
|
+
def make_shareable_if_needed(value)
|
|
24
|
+
if RACTOR_SAFE &&
|
|
25
|
+
(_initialized_ractor.nil? || Ractor.current != _initialized_ractor)
|
|
27
26
|
Ractor.make_shareable(value)
|
|
28
27
|
else
|
|
29
28
|
value
|
|
@@ -17,8 +17,7 @@ module AtomicRuby
|
|
|
17
17
|
@size = size
|
|
18
18
|
@name = name
|
|
19
19
|
|
|
20
|
-
@
|
|
21
|
-
@state = Atom.new(shutdown: false)
|
|
20
|
+
@state = Atom.new(queue: [], shutdown: false)
|
|
22
21
|
@started_threads = Atom.new(0)
|
|
23
22
|
@threads = []
|
|
24
23
|
|
|
@@ -27,8 +26,11 @@ module AtomicRuby
|
|
|
27
26
|
|
|
28
27
|
def <<(work)
|
|
29
28
|
state = @state.swap do |current_state|
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
if current_state[:shutdown]
|
|
30
|
+
current_state
|
|
31
|
+
else
|
|
32
|
+
current_state.merge(queue: [*current_state[:queue], work])
|
|
33
|
+
end
|
|
32
34
|
end
|
|
33
35
|
raise EnqueuedWorkAfterShutdownError if state[:shutdown]
|
|
34
36
|
end
|
|
@@ -36,10 +38,12 @@ module AtomicRuby
|
|
|
36
38
|
def length
|
|
37
39
|
@threads.select(&:alive?).length
|
|
38
40
|
end
|
|
41
|
+
alias size length
|
|
39
42
|
|
|
40
43
|
def queue_length
|
|
41
|
-
@queue.
|
|
44
|
+
@state.value[:queue].length
|
|
42
45
|
end
|
|
46
|
+
alias queue_size queue_length
|
|
43
47
|
|
|
44
48
|
def shutdown
|
|
45
49
|
already_shutdown = false
|
|
@@ -53,7 +57,7 @@ module AtomicRuby
|
|
|
53
57
|
end
|
|
54
58
|
return if already_shutdown
|
|
55
59
|
|
|
56
|
-
Thread.pass until @queue.empty?
|
|
60
|
+
Thread.pass until @state.value[:queue].empty?
|
|
57
61
|
|
|
58
62
|
@threads.each(&:join)
|
|
59
63
|
end
|
|
@@ -74,12 +78,15 @@ module AtomicRuby
|
|
|
74
78
|
should_shutdown = false
|
|
75
79
|
|
|
76
80
|
@state.swap do |current_state|
|
|
77
|
-
if current_state[:shutdown] &&
|
|
81
|
+
if current_state[:shutdown] && current_state[:queue].empty?
|
|
78
82
|
should_shutdown = true
|
|
83
|
+
current_state
|
|
84
|
+
elsif current_state[:queue].empty?
|
|
85
|
+
current_state
|
|
79
86
|
else
|
|
80
|
-
work =
|
|
87
|
+
work = current_state[:queue].first
|
|
88
|
+
current_state.merge(queue: current_state[:queue].drop(1))
|
|
81
89
|
end
|
|
82
|
-
current_state
|
|
83
90
|
end
|
|
84
91
|
|
|
85
92
|
if should_shutdown
|
data/lib/atomic-ruby/version.rb
CHANGED