atomic-ruby 0.7.2 → 0.8.1
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 +10 -0
- data/README.md +34 -23
- data/ext/atomic_ruby/atomic_ruby.c +29 -4
- data/ext/atomic_ruby/atomic_ruby.h +6 -0
- data/lib/atomic-ruby/atom.rb +15 -3
- data/lib/atomic-ruby/atomic_boolean.rb +2 -2
- data/lib/atomic-ruby/atomic_count_down_latch.rb +3 -3
- data/lib/atomic-ruby/atomic_thread_pool.rb +9 -14
- data/lib/atomic-ruby/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ecc5a5a825a6636db5718458318ba2650c05edb9a5a1198947f82e94596a974e
|
|
4
|
+
data.tar.gz: 7a08fbdf2e3362e03450ccb5d2342055c8e50832afae4bc709c5e1c5e187f88b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f52cfbc19feaac870429dc893cbbf3541120e9beeaf20723d9f705b6a74835891473a299b682531d593e5adb1d2b3b75a0eed365b29e3490c1b921ea59aa4714
|
|
7
|
+
data.tar.gz: f797297e13eb693db8759e2bef195ed57288530dd05c8188b67dc1c9df1378476ed4e252b48385a2d951350e6e0f94f6b7aa3a2680d40885759d50fbdcdac1e9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.1] - 2025-11-01
|
|
4
|
+
|
|
5
|
+
- Don't require `AtomicThreadPool#<<` to be given a shareable proc
|
|
6
|
+
|
|
7
|
+
## [0.8.0] - 2025-11-01
|
|
8
|
+
|
|
9
|
+
- Fix Ractor safety (requires Ruby 3.5+)
|
|
10
|
+
- Make `ArgumentError` messages consistent
|
|
11
|
+
- Implement write barriers for `Atom`
|
|
12
|
+
|
|
3
13
|
## [0.7.2] - 2025-10-26
|
|
4
14
|
|
|
5
15
|
- Revert "Fix O(n) performance issue in `AtomicThreadPool#<<` by using linked list"
|
data/README.md
CHANGED
|
@@ -98,7 +98,18 @@ p latch.count #=> 0
|
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
> [!NOTE]
|
|
101
|
-
> `Atom`, `AtomicBoolean`, and `AtomicCountDownLatch` are
|
|
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
|
+
> ```
|
|
102
113
|
|
|
103
114
|
## Benchmarks
|
|
104
115
|
|
|
@@ -210,9 +221,9 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
|
|
|
210
221
|
```
|
|
211
222
|
> bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
|
|
212
223
|
|
|
213
|
-
ruby version: ruby 3.
|
|
224
|
+
ruby version: ruby 3.5.0dev (2025-10-31T18:08:15Z master 980e18496e) +YJIT +PRISM [arm64-darwin25]
|
|
214
225
|
concurrent-ruby version: 1.3.5
|
|
215
|
-
atomic-ruby version: 0.
|
|
226
|
+
atomic-ruby version: 0.8.0
|
|
216
227
|
|
|
217
228
|
Balances:
|
|
218
229
|
Synchronized Bank Account Balance: 975
|
|
@@ -220,9 +231,9 @@ Concurrent Ruby Atomic Bank Account Balance: 975
|
|
|
220
231
|
Atomic Ruby Atomic Bank Account Balance: 975
|
|
221
232
|
|
|
222
233
|
Benchmark Results:
|
|
223
|
-
Synchronized Bank Account: 5.
|
|
224
|
-
Concurrent Ruby Atomic Bank Account: 5.
|
|
225
|
-
Atomic Ruby Atomic Bank Account: 5.
|
|
234
|
+
Synchronized Bank Account: 5.105459 seconds
|
|
235
|
+
Concurrent Ruby Atomic Bank Account: 5.101044 seconds
|
|
236
|
+
Atomic Ruby Atomic Bank Account: 5.091892 seconds
|
|
226
237
|
```
|
|
227
238
|
|
|
228
239
|
</details>
|
|
@@ -301,29 +312,29 @@ end
|
|
|
301
312
|
```
|
|
302
313
|
> bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
|
|
303
314
|
|
|
304
|
-
ruby version: ruby 3.
|
|
315
|
+
ruby version: ruby 3.5.0dev (2025-10-31T18:08:15Z master 980e18496e) +YJIT +PRISM [arm64-darwin25]
|
|
305
316
|
concurrent-ruby version: 1.3.5
|
|
306
|
-
atomic-ruby version: 0.
|
|
317
|
+
atomic-ruby version: 0.8.0
|
|
307
318
|
|
|
308
319
|
Warming up --------------------------------------
|
|
309
320
|
Synchronized Boolean Toggle
|
|
310
|
-
|
|
321
|
+
154.000 i/100ms
|
|
311
322
|
Concurrent Ruby Atomic Boolean Toggle
|
|
312
|
-
|
|
323
|
+
127.000 i/100ms
|
|
313
324
|
Atomic Ruby Atomic Boolean Toggle
|
|
314
|
-
|
|
325
|
+
139.000 i/100ms
|
|
315
326
|
Calculating -------------------------------------
|
|
316
327
|
Synchronized Boolean Toggle
|
|
317
|
-
|
|
328
|
+
1.458k (± 7.3%) i/s (685.85 μs/i) - 7.392k in 5.102733s
|
|
318
329
|
Concurrent Ruby Atomic Boolean Toggle
|
|
319
|
-
|
|
330
|
+
1.129k (± 9.7%) i/s (886.10 μs/i) - 5.588k in 5.001783s
|
|
320
331
|
Atomic Ruby Atomic Boolean Toggle
|
|
321
|
-
|
|
332
|
+
1.476k (± 6.0%) i/s (677.44 μs/i) - 7.367k in 5.017482s
|
|
322
333
|
|
|
323
334
|
Comparison:
|
|
324
|
-
Atomic Ruby Atomic Boolean Toggle:
|
|
325
|
-
Synchronized Boolean Toggle:
|
|
326
|
-
Concurrent Ruby Atomic Boolean Toggle:
|
|
335
|
+
Atomic Ruby Atomic Boolean Toggle: 1476.1 i/s
|
|
336
|
+
Synchronized Boolean Toggle: 1458.1 i/s - same-ish: difference falls within error
|
|
337
|
+
Concurrent Ruby Atomic Boolean Toggle: 1128.5 i/s - 1.31x slower
|
|
327
338
|
```
|
|
328
339
|
|
|
329
340
|
</details>
|
|
@@ -351,11 +362,11 @@ results = []
|
|
|
351
362
|
end
|
|
352
363
|
|
|
353
364
|
100.times do
|
|
354
|
-
pool <<
|
|
365
|
+
pool << proc { sleep(0.2) }
|
|
355
366
|
end
|
|
356
367
|
|
|
357
368
|
100.times do
|
|
358
|
-
pool <<
|
|
369
|
+
pool << proc { 1_000_000.times.map(&:itself).sum }
|
|
359
370
|
end
|
|
360
371
|
|
|
361
372
|
pool.shutdown
|
|
@@ -379,13 +390,13 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
|
|
|
379
390
|
```
|
|
380
391
|
> bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
|
|
381
392
|
|
|
382
|
-
ruby version: ruby 3.
|
|
393
|
+
ruby version: ruby 3.5.0dev (2025-10-31T18:08:15Z master 980e18496e) +YJIT +PRISM [arm64-darwin25]
|
|
383
394
|
concurrent-ruby version: 1.3.5
|
|
384
|
-
atomic-ruby version: 0.
|
|
395
|
+
atomic-ruby version: 0.8.1
|
|
385
396
|
|
|
386
397
|
Benchmark Results:
|
|
387
|
-
Concurrent Ruby Thread Pool: 5.
|
|
388
|
-
Atomic Ruby Atomic Thread Pool:
|
|
398
|
+
Concurrent Ruby Thread Pool: 5.139026 seconds
|
|
399
|
+
Atomic Ruby Atomic Thread Pool: 4.833597 seconds
|
|
389
400
|
```
|
|
390
401
|
|
|
391
402
|
</details>
|
|
@@ -31,20 +31,35 @@ static const rb_data_type_t atomic_ruby_atom_type = {
|
|
|
31
31
|
.dsize = atomic_ruby_atom_memsize,
|
|
32
32
|
.dcompact = atomic_ruby_atom_compact
|
|
33
33
|
},
|
|
34
|
-
|
|
34
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
35
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FROZEN_SHAREABLE
|
|
36
|
+
#else
|
|
37
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
|
|
38
|
+
#endif
|
|
35
39
|
};
|
|
36
40
|
|
|
37
41
|
static VALUE rb_cAtom_allocate(VALUE klass) {
|
|
38
42
|
atomic_ruby_atom_t *atomic_ruby_atom;
|
|
39
43
|
VALUE obj = TypedData_Make_Struct(klass, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
|
|
40
|
-
atomic_ruby_atom->value
|
|
44
|
+
RB_OBJ_WRITE(obj, &atomic_ruby_atom->value, Qnil);
|
|
41
45
|
return obj;
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
49
|
+
static void check_value_shareable(VALUE value) {
|
|
50
|
+
if (!rb_ractor_shareable_p(value)) {
|
|
51
|
+
rb_raise(rb_eArgError, "value must be a shareable object");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
#endif
|
|
55
|
+
|
|
44
56
|
static VALUE rb_cAtom_initialize(VALUE self, VALUE value) {
|
|
45
57
|
atomic_ruby_atom_t *atomic_ruby_atom;
|
|
46
58
|
TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
|
|
47
|
-
|
|
59
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
60
|
+
check_value_shareable(value);
|
|
61
|
+
#endif
|
|
62
|
+
RB_OBJ_WRITE(self, &atomic_ruby_atom->value, value);
|
|
48
63
|
return self;
|
|
49
64
|
}
|
|
50
65
|
|
|
@@ -62,13 +77,17 @@ static VALUE rb_cAtom_swap(VALUE self) {
|
|
|
62
77
|
do {
|
|
63
78
|
expected_old_value = atomic_ruby_atom->value;
|
|
64
79
|
new_value = rb_yield(expected_old_value);
|
|
80
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
81
|
+
check_value_shareable(new_value);
|
|
82
|
+
#endif
|
|
65
83
|
} while (RUBY_ATOMIC_VALUE_CAS(atomic_ruby_atom->value, expected_old_value, new_value) != expected_old_value);
|
|
84
|
+
RB_OBJ_WRITTEN(self, expected_old_value, new_value);
|
|
66
85
|
|
|
67
86
|
return new_value;
|
|
68
87
|
}
|
|
69
88
|
|
|
70
89
|
RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
|
|
71
|
-
#ifdef
|
|
90
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
72
91
|
rb_ext_ractor_safe(true);
|
|
73
92
|
#endif
|
|
74
93
|
|
|
@@ -79,4 +98,10 @@ RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
|
|
|
79
98
|
rb_define_method(rb_cAtom, "_initialize", rb_cAtom_initialize, 1);
|
|
80
99
|
rb_define_method(rb_cAtom, "_value", rb_cAtom_value, 0);
|
|
81
100
|
rb_define_method(rb_cAtom, "_swap", rb_cAtom_swap, 0);
|
|
101
|
+
|
|
102
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
103
|
+
rb_define_const(rb_mAtomicRuby, "RACTOR_SAFE", Qtrue);
|
|
104
|
+
#else
|
|
105
|
+
rb_define_const(rb_mAtomicRuby, "RACTOR_SAFE", Qfalse);
|
|
106
|
+
#endif
|
|
82
107
|
}
|
data/lib/atomic-ruby/atom.rb
CHANGED
|
@@ -5,9 +5,9 @@ require "atomic_ruby/atomic_ruby"
|
|
|
5
5
|
module AtomicRuby
|
|
6
6
|
class Atom
|
|
7
7
|
def initialize(value)
|
|
8
|
-
_initialize(value)
|
|
8
|
+
_initialize(make_shareable(value))
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
freeze if RACTOR_SAFE
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def value
|
|
@@ -15,7 +15,19 @@ module AtomicRuby
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def swap(&block)
|
|
18
|
-
_swap
|
|
18
|
+
_swap do |old_value|
|
|
19
|
+
make_shareable(block.call(old_value))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def make_shareable(value)
|
|
26
|
+
if RACTOR_SAFE
|
|
27
|
+
Ractor.make_shareable(value)
|
|
28
|
+
else
|
|
29
|
+
value
|
|
30
|
+
end
|
|
19
31
|
end
|
|
20
32
|
end
|
|
21
33
|
end
|
|
@@ -6,12 +6,12 @@ module AtomicRuby
|
|
|
6
6
|
class AtomicBoolean
|
|
7
7
|
def initialize(boolean)
|
|
8
8
|
unless boolean.is_a?(TrueClass) || boolean.is_a?(FalseClass)
|
|
9
|
-
raise ArgumentError, "boolean must be a TrueClass or FalseClass
|
|
9
|
+
raise ArgumentError, "boolean must be a TrueClass or FalseClass"
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
@boolean = Atom.new(boolean)
|
|
13
13
|
|
|
14
|
-
Ractor.make_shareable(self)
|
|
14
|
+
Ractor.make_shareable(self) if RACTOR_SAFE
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def value
|
|
@@ -9,12 +9,12 @@ module AtomicRuby
|
|
|
9
9
|
|
|
10
10
|
def initialize(count)
|
|
11
11
|
unless count.is_a?(Integer) && count > 0
|
|
12
|
-
raise ArgumentError, "count must be a positive Integer
|
|
12
|
+
raise ArgumentError, "count must be a positive Integer"
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
@count = Atom.new(count)
|
|
16
16
|
|
|
17
|
-
Ractor.make_shareable(self)
|
|
17
|
+
Ractor.make_shareable(self) if RACTOR_SAFE
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def count
|
|
@@ -32,7 +32,7 @@ module AtomicRuby
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
raise AlreadyCountedDownError, "already counted down to zero" if already_counted_down
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
new_count
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -17,7 +17,8 @@ module AtomicRuby
|
|
|
17
17
|
@size = size
|
|
18
18
|
@name = name
|
|
19
19
|
|
|
20
|
-
@
|
|
20
|
+
@queue = Queue.new
|
|
21
|
+
@state = Atom.new(shutdown: false)
|
|
21
22
|
@started_threads = Atom.new(0)
|
|
22
23
|
@threads = []
|
|
23
24
|
|
|
@@ -26,11 +27,8 @@ module AtomicRuby
|
|
|
26
27
|
|
|
27
28
|
def <<(work)
|
|
28
29
|
state = @state.swap do |current_state|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
else
|
|
32
|
-
current_state.merge(queue: [*current_state[:queue], work])
|
|
33
|
-
end
|
|
30
|
+
@queue.push(work) unless current_state[:shutdown]
|
|
31
|
+
current_state
|
|
34
32
|
end
|
|
35
33
|
raise EnqueuedWorkAfterShutdownError if state[:shutdown]
|
|
36
34
|
end
|
|
@@ -40,7 +38,7 @@ module AtomicRuby
|
|
|
40
38
|
end
|
|
41
39
|
|
|
42
40
|
def queue_length
|
|
43
|
-
@
|
|
41
|
+
@queue.size
|
|
44
42
|
end
|
|
45
43
|
|
|
46
44
|
def shutdown
|
|
@@ -55,7 +53,7 @@ module AtomicRuby
|
|
|
55
53
|
end
|
|
56
54
|
return if already_shutdown
|
|
57
55
|
|
|
58
|
-
Thread.pass until @
|
|
56
|
+
Thread.pass until @queue.empty?
|
|
59
57
|
|
|
60
58
|
@threads.each(&:join)
|
|
61
59
|
end
|
|
@@ -76,15 +74,12 @@ module AtomicRuby
|
|
|
76
74
|
should_shutdown = false
|
|
77
75
|
|
|
78
76
|
@state.swap do |current_state|
|
|
79
|
-
if current_state[:shutdown] &&
|
|
77
|
+
if current_state[:shutdown] && @queue.empty?
|
|
80
78
|
should_shutdown = true
|
|
81
|
-
current_state
|
|
82
|
-
elsif current_state[:queue].empty?
|
|
83
|
-
current_state
|
|
84
79
|
else
|
|
85
|
-
work =
|
|
86
|
-
current_state.merge(queue: current_state[:queue].drop(1))
|
|
80
|
+
work = @queue.pop(timeout: 0)
|
|
87
81
|
end
|
|
82
|
+
current_state
|
|
88
83
|
end
|
|
89
84
|
|
|
90
85
|
if should_shutdown
|
data/lib/atomic-ruby/version.rb
CHANGED
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.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua Young
|
|
@@ -50,7 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
50
50
|
- !ruby/object:Gem::Version
|
|
51
51
|
version: '0'
|
|
52
52
|
requirements: []
|
|
53
|
-
rubygems_version:
|
|
53
|
+
rubygems_version: 4.0.0.dev
|
|
54
54
|
specification_version: 4
|
|
55
55
|
summary: Atomic primitives for Ruby
|
|
56
56
|
test_files: []
|