atomic-ruby 0.7.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df0115a62c1857c8f5c11cac42003d5ed1c91ec1c5b204d5b5cf093f25d5cab9
4
- data.tar.gz: 67963d4c342b33e235a5cb320013388dc227d46311c80c1b19e029b4ff02bc1d
3
+ metadata.gz: 9e35d6cd7774994256c00cbf310fa5511aaafac0c55e4b0919102f22b444961d
4
+ data.tar.gz: 763d472bdc50d97522f9a9f630f6d51f2db4832f9d507ca27d0f6dbae761ca54
5
5
  SHA512:
6
- metadata.gz: e26adf803f4e798808a736eabdb86dc8d4074a71231aa2806e15f84ea5c8c6a7770cc268fb412733702fb0c540d1392e01265bf3571c3d143165f51776f72c19
7
- data.tar.gz: d788e9e25c29f26050095050357ddb64b31a132bd1c9d9e13604945eec2382744a0b94e9f946d97eec6721c73fa16d83e574fc5e3e0644a3101c8b97b9b9b833
6
+ metadata.gz: a39511160e42082e08f9d435a29ffe658325d0e93fa10818a6f60b5f54882bf698ce2d2dcd655a6cbde19bea9cd7ebc2ede86fd8ff5e70e95bb94750400a7f36
7
+ data.tar.gz: 89800c8d9fe359a497be8a71c463e3d22ef1585752d3320c85aeae0051634b41f70c78ace27502b452231b088de4a8e359ac7ab5be8ffa71836cdf564c9c2240
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## [0.7.2] - 2025-10-26
4
10
 
5
11
  - Revert "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 also Ractor safe.
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.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin25]
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.7.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.102692 seconds
224
- Concurrent Ruby Atomic Bank Account: 5.100103 seconds
225
- Atomic Ruby Atomic Bank Account: 5.096461 seconds
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.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin25]
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.7.0
318
+ atomic-ruby version: 0.8.0
307
319
 
308
320
  Warming up --------------------------------------
309
321
  Synchronized Boolean Toggle
310
- 93.000 i/100ms
322
+ 154.000 i/100ms
311
323
  Concurrent Ruby Atomic Boolean Toggle
312
- 79.000 i/100ms
324
+ 127.000 i/100ms
313
325
  Atomic Ruby Atomic Boolean Toggle
314
- 87.000 i/100ms
326
+ 139.000 i/100ms
315
327
  Calculating -------------------------------------
316
328
  Synchronized Boolean Toggle
317
- 889.613 (± 3.0%) i/s (1.12 ms/i) - 4.464k in 5.022732s
329
+ 1.458k7.3%) i/s (685.85 μs/i) - 7.392k in 5.102733s
318
330
  Concurrent Ruby Atomic Boolean Toggle
319
- 803.4182.5%) i/s (1.24 ms/i) - 4.029k in 5.017952s
331
+ 1.129k9.7%) i/s (886.10 μs/i) - 5.588k in 5.001783s
320
332
  Atomic Ruby Atomic Boolean Toggle
321
- 1.037k3.1%) i/s (964.07 μs/i) - 5.220k in 5.037558s
333
+ 1.476k6.0%) i/s (677.44 μs/i) - 7.367k in 5.017482s
322
334
 
323
335
  Comparison:
324
- Atomic Ruby Atomic Boolean Toggle: 1037.3 i/s
325
- Synchronized Boolean Toggle: 889.6 i/s - 1.17x slower
326
- Concurrent Ruby Atomic Boolean Toggle: 803.4 i/s - 1.29x slower
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 << -> { sleep(0.2) }
374
+ pool << shareable_proc { sleep(0.2) }
355
375
  end
356
376
 
357
377
  100.times do
358
- pool << -> { 1_000_000.times.map(&:itself).sum }
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.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin25]
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.7.0
404
+ atomic-ruby version: 0.8.0
385
405
 
386
406
  Benchmark Results:
387
- Concurrent Ruby Thread Pool: 5.30284 seconds
388
- Atomic Ruby Atomic Thread Pool: 5.019147 seconds
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
- .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_FROZEN_SHAREABLE
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 = Qnil;
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
- atomic_ruby_atom->value = value;
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 HAVE_RB_EXT_RACTOR_SAFE
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
  }
@@ -3,5 +3,11 @@
3
3
 
4
4
  #include "ruby.h"
5
5
  #include "ruby/atomic.h"
6
+ #include "ruby/ractor.h"
7
+ #include "ruby/version.h"
8
+
9
+ #if RUBY_API_VERSION_CODE >= 30500
10
+ #define ATOMIC_RUBY_RACTOR_SAFE 1
11
+ #endif
6
12
 
7
13
  #endif /* ATOMIC_RUBY_ATOM_H */
@@ -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
- Ractor.make_shareable(self)
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(&block)
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, got #{boolean.class}"
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, got #{count.class}"
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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.7.2"
4
+ VERSION = "0.8.0"
5
5
  end
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.7.2
4
+ version: 0.8.0
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: 3.7.2
53
+ rubygems_version: 4.0.0.dev
54
54
  specification_version: 4
55
55
  summary: Atomic primitives for Ruby
56
56
  test_files: []