atomic-ruby 0.7.1 → 0.8.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 +10 -0
- data/README.md +43 -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 +3 -4
- data/lib/atomic-ruby/version.rb +1 -1
- metadata +2 -3
- data/lib/atomic-ruby/linked_list.rb +0 -37
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e35d6cd7774994256c00cbf310fa5511aaafac0c55e4b0919102f22b444961d
|
|
4
|
+
data.tar.gz: 763d472bdc50d97522f9a9f630f6d51f2db4832f9d507ca27d0f6dbae761ca54
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a39511160e42082e08f9d435a29ffe658325d0e93fa10818a6f60b5f54882bf698ce2d2dcd655a6cbde19bea9cd7ebc2ede86fd8ff5e70e95bb94750400a7f36
|
|
7
|
+
data.tar.gz: 89800c8d9fe359a497be8a71c463e3d22ef1585752d3320c85aeae0051634b41f70c78ace27502b452231b088de4a8e359ac7ab5be8ffa71836cdf564c9c2240
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2025-11-01
|
|
4
|
+
|
|
5
|
+
- Fix Ractor safety (requires Ruby 3.5+)
|
|
6
|
+
- Make `ArgumentError` messages consistent
|
|
7
|
+
- Implement write barriers for `Atom`
|
|
8
|
+
|
|
9
|
+
## [0.7.2] - 2025-10-26
|
|
10
|
+
|
|
11
|
+
- Revert "Fix O(n) performance issue in `AtomicThreadPool#<<` by using linked list"
|
|
12
|
+
|
|
3
13
|
## [0.7.1] - 2025-10-26
|
|
4
14
|
|
|
5
15
|
- Fix O(n) performance issue in `AtomicThreadPool#<<` by using linked list
|
data/README.md
CHANGED
|
@@ -98,7 +98,19 @@ 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 (including queueing them with `AtomicThreadPool#<<`), create them
|
|
104
|
+
> using `Ractor.shareable_proc`, as `Ractor.make_shareable` cannot convert regular procs to
|
|
105
|
+
> shareable ones when the proc's context is not shareable.
|
|
106
|
+
>
|
|
107
|
+
> ```ruby
|
|
108
|
+
> # This will raise an error in Ruby 3.5+ (proc created in non-shareable context)
|
|
109
|
+
> atom = Atom.new(proc { puts "hello" })
|
|
110
|
+
>
|
|
111
|
+
> # Use this instead
|
|
112
|
+
> atom = Atom.new(Ractor.shareable_proc { puts "hello" })
|
|
113
|
+
> ```
|
|
102
114
|
|
|
103
115
|
## Benchmarks
|
|
104
116
|
|
|
@@ -210,9 +222,9 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
|
|
|
210
222
|
```
|
|
211
223
|
> bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
|
|
212
224
|
|
|
213
|
-
ruby version: ruby 3.
|
|
225
|
+
ruby version: ruby 3.5.0dev (2025-10-31T18:08:15Z master 980e18496e) +YJIT +PRISM [arm64-darwin25]
|
|
214
226
|
concurrent-ruby version: 1.3.5
|
|
215
|
-
atomic-ruby version: 0.
|
|
227
|
+
atomic-ruby version: 0.8.0
|
|
216
228
|
|
|
217
229
|
Balances:
|
|
218
230
|
Synchronized Bank Account Balance: 975
|
|
@@ -220,9 +232,9 @@ Concurrent Ruby Atomic Bank Account Balance: 975
|
|
|
220
232
|
Atomic Ruby Atomic Bank Account Balance: 975
|
|
221
233
|
|
|
222
234
|
Benchmark Results:
|
|
223
|
-
Synchronized Bank Account: 5.
|
|
224
|
-
Concurrent Ruby Atomic Bank Account: 5.
|
|
225
|
-
Atomic Ruby Atomic Bank Account: 5.
|
|
235
|
+
Synchronized Bank Account: 5.105459 seconds
|
|
236
|
+
Concurrent Ruby Atomic Bank Account: 5.101044 seconds
|
|
237
|
+
Atomic Ruby Atomic Bank Account: 5.091892 seconds
|
|
226
238
|
```
|
|
227
239
|
|
|
228
240
|
</details>
|
|
@@ -301,29 +313,29 @@ end
|
|
|
301
313
|
```
|
|
302
314
|
> bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
|
|
303
315
|
|
|
304
|
-
ruby version: ruby 3.
|
|
316
|
+
ruby version: ruby 3.5.0dev (2025-10-31T18:08:15Z master 980e18496e) +YJIT +PRISM [arm64-darwin25]
|
|
305
317
|
concurrent-ruby version: 1.3.5
|
|
306
|
-
atomic-ruby version: 0.
|
|
318
|
+
atomic-ruby version: 0.8.0
|
|
307
319
|
|
|
308
320
|
Warming up --------------------------------------
|
|
309
321
|
Synchronized Boolean Toggle
|
|
310
|
-
|
|
322
|
+
154.000 i/100ms
|
|
311
323
|
Concurrent Ruby Atomic Boolean Toggle
|
|
312
|
-
|
|
324
|
+
127.000 i/100ms
|
|
313
325
|
Atomic Ruby Atomic Boolean Toggle
|
|
314
|
-
|
|
326
|
+
139.000 i/100ms
|
|
315
327
|
Calculating -------------------------------------
|
|
316
328
|
Synchronized Boolean Toggle
|
|
317
|
-
|
|
329
|
+
1.458k (± 7.3%) i/s (685.85 μs/i) - 7.392k in 5.102733s
|
|
318
330
|
Concurrent Ruby Atomic Boolean Toggle
|
|
319
|
-
|
|
331
|
+
1.129k (± 9.7%) i/s (886.10 μs/i) - 5.588k in 5.001783s
|
|
320
332
|
Atomic Ruby Atomic Boolean Toggle
|
|
321
|
-
|
|
333
|
+
1.476k (± 6.0%) i/s (677.44 μs/i) - 7.367k in 5.017482s
|
|
322
334
|
|
|
323
335
|
Comparison:
|
|
324
|
-
Atomic Ruby Atomic Boolean Toggle:
|
|
325
|
-
Synchronized Boolean Toggle:
|
|
326
|
-
Concurrent Ruby Atomic Boolean Toggle:
|
|
336
|
+
Atomic Ruby Atomic Boolean Toggle: 1476.1 i/s
|
|
337
|
+
Synchronized Boolean Toggle: 1458.1 i/s - same-ish: difference falls within error
|
|
338
|
+
Concurrent Ruby Atomic Boolean Toggle: 1128.5 i/s - 1.31x slower
|
|
327
339
|
```
|
|
328
340
|
|
|
329
341
|
</details>
|
|
@@ -341,6 +353,14 @@ require "benchmark"
|
|
|
341
353
|
require "concurrent-ruby"
|
|
342
354
|
require_relative "../lib/atomic-ruby"
|
|
343
355
|
|
|
356
|
+
def shareable_proc(&block)
|
|
357
|
+
if AtomicRuby::RACTOR_SAFE
|
|
358
|
+
Ractor.shareable_proc(&block)
|
|
359
|
+
else
|
|
360
|
+
block
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
344
364
|
results = []
|
|
345
365
|
|
|
346
366
|
2.times do |idx|
|
|
@@ -351,11 +371,11 @@ results = []
|
|
|
351
371
|
end
|
|
352
372
|
|
|
353
373
|
100.times do
|
|
354
|
-
pool <<
|
|
374
|
+
pool << shareable_proc { sleep(0.2) }
|
|
355
375
|
end
|
|
356
376
|
|
|
357
377
|
100.times do
|
|
358
|
-
pool <<
|
|
378
|
+
pool << shareable_proc { 1_000_000.times.map(&:itself).sum }
|
|
359
379
|
end
|
|
360
380
|
|
|
361
381
|
pool.shutdown
|
|
@@ -379,13 +399,13 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
|
|
|
379
399
|
```
|
|
380
400
|
> bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
|
|
381
401
|
|
|
382
|
-
ruby version: ruby 3.
|
|
402
|
+
ruby version: ruby 3.5.0dev (2025-10-31T18:08:15Z master 980e18496e) +YJIT +PRISM [arm64-darwin25]
|
|
383
403
|
concurrent-ruby version: 1.3.5
|
|
384
|
-
atomic-ruby version: 0.
|
|
404
|
+
atomic-ruby version: 0.8.0
|
|
385
405
|
|
|
386
406
|
Benchmark Results:
|
|
387
|
-
Concurrent Ruby Thread Pool:
|
|
388
|
-
Atomic Ruby Atomic Thread Pool: 4.
|
|
407
|
+
Concurrent Ruby Thread Pool: 5.13772 seconds
|
|
408
|
+
Atomic Ruby Atomic Thread Pool: 4.893086 seconds
|
|
389
409
|
```
|
|
390
410
|
|
|
391
411
|
</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
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "atom"
|
|
4
|
-
require_relative "linked_list"
|
|
5
4
|
|
|
6
5
|
module AtomicRuby
|
|
7
6
|
class AtomicThreadPool
|
|
@@ -18,7 +17,7 @@ module AtomicRuby
|
|
|
18
17
|
@size = size
|
|
19
18
|
@name = name
|
|
20
19
|
|
|
21
|
-
@state = Atom.new(queue:
|
|
20
|
+
@state = Atom.new(queue: [], shutdown: false)
|
|
22
21
|
@started_threads = Atom.new(0)
|
|
23
22
|
@threads = []
|
|
24
23
|
|
|
@@ -30,7 +29,7 @@ module AtomicRuby
|
|
|
30
29
|
if current_state[:shutdown]
|
|
31
30
|
current_state
|
|
32
31
|
else
|
|
33
|
-
current_state.merge(queue: current_state[:queue]
|
|
32
|
+
current_state.merge(queue: [*current_state[:queue], work])
|
|
34
33
|
end
|
|
35
34
|
end
|
|
36
35
|
raise EnqueuedWorkAfterShutdownError if state[:shutdown]
|
|
@@ -84,7 +83,7 @@ module AtomicRuby
|
|
|
84
83
|
current_state
|
|
85
84
|
else
|
|
86
85
|
work = current_state[:queue].first
|
|
87
|
-
current_state.merge(queue: current_state[:queue].
|
|
86
|
+
current_state.merge(queue: current_state[:queue].drop(1))
|
|
88
87
|
end
|
|
89
88
|
end
|
|
90
89
|
|
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.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua Young
|
|
@@ -29,7 +29,6 @@ files:
|
|
|
29
29
|
- lib/atomic-ruby/atomic_boolean.rb
|
|
30
30
|
- lib/atomic-ruby/atomic_count_down_latch.rb
|
|
31
31
|
- lib/atomic-ruby/atomic_thread_pool.rb
|
|
32
|
-
- lib/atomic-ruby/linked_list.rb
|
|
33
32
|
- lib/atomic-ruby/version.rb
|
|
34
33
|
homepage: https://github.com/joshuay03/atomic-ruby
|
|
35
34
|
licenses:
|
|
@@ -51,7 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
51
50
|
- !ruby/object:Gem::Version
|
|
52
51
|
version: '0'
|
|
53
52
|
requirements: []
|
|
54
|
-
rubygems_version:
|
|
53
|
+
rubygems_version: 4.0.0.dev
|
|
55
54
|
specification_version: 4
|
|
56
55
|
summary: Atomic primitives for Ruby
|
|
57
56
|
test_files: []
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module AtomicRuby
|
|
4
|
-
class LinkedList
|
|
5
|
-
Node = Data.define(:value, :next_node)
|
|
6
|
-
|
|
7
|
-
def initialize(head = nil)
|
|
8
|
-
@head = head
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def prepend(value)
|
|
12
|
-
self.class.new(Node.new(value, @head))
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def first
|
|
16
|
-
@head&.value
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def rest
|
|
20
|
-
self.class.new(@head&.next_node)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def empty?
|
|
24
|
-
@head.nil?
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def length
|
|
28
|
-
count = 0
|
|
29
|
-
current = @head
|
|
30
|
-
while current
|
|
31
|
-
count += 1
|
|
32
|
-
current = current.next_node
|
|
33
|
-
end
|
|
34
|
-
count
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|