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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df0115a62c1857c8f5c11cac42003d5ed1c91ec1c5b204d5b5cf093f25d5cab9
4
- data.tar.gz: 67963d4c342b33e235a5cb320013388dc227d46311c80c1b19e029b4ff02bc1d
3
+ metadata.gz: ecc5a5a825a6636db5718458318ba2650c05edb9a5a1198947f82e94596a974e
4
+ data.tar.gz: 7a08fbdf2e3362e03450ccb5d2342055c8e50832afae4bc709c5e1c5e187f88b
5
5
  SHA512:
6
- metadata.gz: e26adf803f4e798808a736eabdb86dc8d4074a71231aa2806e15f84ea5c8c6a7770cc268fb412733702fb0c540d1392e01265bf3571c3d143165f51776f72c19
7
- data.tar.gz: d788e9e25c29f26050095050357ddb64b31a132bd1c9d9e13604945eec2382744a0b94e9f946d97eec6721c73fa16d83e574fc5e3e0644a3101c8b97b9b9b833
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 also Ractor safe.
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.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin25]
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.7.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.102692 seconds
224
- Concurrent Ruby Atomic Bank Account: 5.100103 seconds
225
- Atomic Ruby Atomic Bank Account: 5.096461 seconds
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.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin25]
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.7.0
317
+ atomic-ruby version: 0.8.0
307
318
 
308
319
  Warming up --------------------------------------
309
320
  Synchronized Boolean Toggle
310
- 93.000 i/100ms
321
+ 154.000 i/100ms
311
322
  Concurrent Ruby Atomic Boolean Toggle
312
- 79.000 i/100ms
323
+ 127.000 i/100ms
313
324
  Atomic Ruby Atomic Boolean Toggle
314
- 87.000 i/100ms
325
+ 139.000 i/100ms
315
326
  Calculating -------------------------------------
316
327
  Synchronized Boolean Toggle
317
- 889.613 (± 3.0%) i/s (1.12 ms/i) - 4.464k in 5.022732s
328
+ 1.458k7.3%) i/s (685.85 μs/i) - 7.392k in 5.102733s
318
329
  Concurrent Ruby Atomic Boolean Toggle
319
- 803.4182.5%) i/s (1.24 ms/i) - 4.029k in 5.017952s
330
+ 1.129k9.7%) i/s (886.10 μs/i) - 5.588k in 5.001783s
320
331
  Atomic Ruby Atomic Boolean Toggle
321
- 1.037k3.1%) i/s (964.07 μs/i) - 5.220k in 5.037558s
332
+ 1.476k6.0%) i/s (677.44 μs/i) - 7.367k in 5.017482s
322
333
 
323
334
  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
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 << -> { sleep(0.2) }
365
+ pool << proc { sleep(0.2) }
355
366
  end
356
367
 
357
368
  100.times do
358
- pool << -> { 1_000_000.times.map(&:itself).sum }
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.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin25]
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.7.0
395
+ atomic-ruby version: 0.8.1
385
396
 
386
397
  Benchmark Results:
387
- Concurrent Ruby Thread Pool: 5.30284 seconds
388
- Atomic Ruby Atomic Thread Pool: 5.019147 seconds
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
- .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
 
@@ -17,7 +17,8 @@ module AtomicRuby
17
17
  @size = size
18
18
  @name = name
19
19
 
20
- @state = Atom.new(queue: [], shutdown: false)
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
- if current_state[:shutdown]
30
- current_state
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
- @state.value[:queue].length
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 @state.value[:queue].empty?
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] && current_state[:queue].empty?
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 = current_state[:queue].first
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
@@ -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.1"
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.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: 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: []