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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecc5a5a825a6636db5718458318ba2650c05edb9a5a1198947f82e94596a974e
4
- data.tar.gz: 7a08fbdf2e3362e03450ccb5d2342055c8e50832afae4bc709c5e1c5e187f88b
3
+ metadata.gz: 6bef30ee19f89bf213e837f5ee565b4e0b2f926c422dd7204270f349a331142c
4
+ data.tar.gz: b19ac4ac6c8f363c891aae5c7ef4b4c39c80b420673f71890fe8c4f1974dee89
5
5
  SHA512:
6
- metadata.gz: f52cfbc19feaac870429dc893cbbf3541120e9beeaf20723d9f705b6a74835891473a299b682531d593e5adb1d2b3b75a0eed365b29e3490c1b921ea59aa4714
7
- data.tar.gz: f797297e13eb693db8759e2bef195ed57288530dd05c8188b67dc1c9df1378476ed4e252b48385a2d951350e6e0f94f6b7aa3a2680d40885759d50fbdcdac1e9
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-10-31T18:08:15Z master 980e18496e) +YJIT +PRISM [arm64-darwin25]
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.8.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.105459 seconds
235
- Concurrent Ruby Atomic Bank Account: 5.101044 seconds
236
- Atomic Ruby Atomic Bank Account: 5.091892 seconds
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-10-31T18:08:15Z master 980e18496e) +YJIT +PRISM [arm64-darwin25]
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.8.0
306
+ atomic-ruby version: 0.9.0
318
307
 
319
308
  Warming up --------------------------------------
320
309
  Synchronized Boolean Toggle
321
- 154.000 i/100ms
310
+ 158.000 i/100ms
322
311
  Concurrent Ruby Atomic Boolean Toggle
323
- 127.000 i/100ms
312
+ 113.000 i/100ms
324
313
  Atomic Ruby Atomic Boolean Toggle
325
- 139.000 i/100ms
314
+ 122.000 i/100ms
326
315
  Calculating -------------------------------------
327
316
  Synchronized Boolean Toggle
328
- 1.458k7.3%) i/s (685.85 μs/i) - 7.392k in 5.102733s
317
+ 1.521k2.1%) i/s (657.49 μs/i) - 7.742k in 5.092579s
329
318
  Concurrent Ruby Atomic Boolean Toggle
330
- 1.129k9.7%) i/s (886.10 μs/i) - 5.588k in 5.001783s
319
+ 1.141k1.6%) i/s (876.12 μs/i) - 5.763k in 5.050298s
331
320
  Atomic Ruby Atomic Boolean Toggle
332
- 1.476k6.0%) i/s (677.44 μs/i) - 7.367k in 5.017482s
321
+ 1.243k1.3%) i/s (804.64 μs/i) - 6.222k in 5.007246s
333
322
 
334
323
  Comparison:
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
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-10-31T18:08:15Z master 980e18496e) +YJIT +PRISM [arm64-darwin25]
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.8.1
384
+ atomic-ruby version: 0.9.0
396
385
 
397
386
  Benchmark Results:
398
- Concurrent Ruby Thread Pool: 5.139026 seconds
399
- Atomic Ruby Atomic Thread Pool: 4.833597 seconds
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
- 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
- }
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
- check_value_shareable(value);
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
- return (VALUE)RUBY_ATOMIC_PTR_LOAD(atomic_ruby_atom->value);
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
- check_value_shareable(new_value);
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 */
@@ -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")
@@ -5,9 +5,7 @@ require "atomic_ruby/atomic_ruby"
5
5
  module AtomicRuby
6
6
  class Atom
7
7
  def initialize(value)
8
- _initialize(make_shareable(value))
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
- make_shareable(block.call(old_value))
17
+ make_shareable_if_needed(block.call(old_value))
20
18
  end
21
19
  end
22
20
 
23
21
  private
24
22
 
25
- def make_shareable(value)
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
- @queue = Queue.new
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
- @queue.push(work) unless current_state[:shutdown]
31
- current_state
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.size
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] && @queue.empty?
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 = @queue.pop(timeout: 0)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.8.1"
4
+ VERSION = "0.9.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.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young