atomic-ruby 0.8.0 → 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: 9e35d6cd7774994256c00cbf310fa5511aaafac0c55e4b0919102f22b444961d
4
- data.tar.gz: 763d472bdc50d97522f9a9f630f6d51f2db4832f9d507ca27d0f6dbae761ca54
3
+ metadata.gz: 6bef30ee19f89bf213e837f5ee565b4e0b2f926c422dd7204270f349a331142c
4
+ data.tar.gz: b19ac4ac6c8f363c891aae5c7ef4b4c39c80b420673f71890fe8c4f1974dee89
5
5
  SHA512:
6
- metadata.gz: a39511160e42082e08f9d435a29ffe658325d0e93fa10818a6f60b5f54882bf698ce2d2dcd655a6cbde19bea9cd7ebc2ede86fd8ff5e70e95bb94750400a7f36
7
- data.tar.gz: 89800c8d9fe359a497be8a71c463e3d22ef1585752d3320c85aeae0051634b41f70c78ace27502b452231b088de4a8e359ac7ab5be8ffa71836cdf564c9c2240
6
+ metadata.gz: 8afd84e8e6cc724949c88c6485bd51e8d3ba86e56852ad742136613ce2f4a36468de88b7c011a2061218ff7edb189a8e8a13f3049741d5a020862c8cc9760e7d
7
+ data.tar.gz: d5727d75e5b3c6f4cce6c7dd394b1fd7d64d48f3940124a27f2725e1947c6aac375604fb98364a24aa6bbcb50670680b3b755c566f5092829b21eff7bef98d10
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
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
+
9
+ ## [0.8.1] - 2025-11-01
10
+
11
+ - Don't require `AtomicThreadPool#<<` to be given a shareable proc
12
+
3
13
  ## [0.8.0] - 2025-11-01
4
14
 
5
15
  - Fix Ractor safety (requires Ruby 3.5+)
data/README.md CHANGED
@@ -99,18 +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 (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
- > ```
114
102
 
115
103
  ## Benchmarks
116
104
 
@@ -222,9 +210,9 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
222
210
  ```
223
211
  > bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
224
212
 
225
- 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]
226
214
  concurrent-ruby version: 1.3.5
227
- atomic-ruby version: 0.8.0
215
+ atomic-ruby version: 0.9.0
228
216
 
229
217
  Balances:
230
218
  Synchronized Bank Account Balance: 975
@@ -232,9 +220,9 @@ Concurrent Ruby Atomic Bank Account Balance: 975
232
220
  Atomic Ruby Atomic Bank Account Balance: 975
233
221
 
234
222
  Benchmark Results:
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
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
238
226
  ```
239
227
 
240
228
  </details>
@@ -313,29 +301,29 @@ end
313
301
  ```
314
302
  > bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
315
303
 
316
- 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]
317
305
  concurrent-ruby version: 1.3.5
318
- atomic-ruby version: 0.8.0
306
+ atomic-ruby version: 0.9.0
319
307
 
320
308
  Warming up --------------------------------------
321
309
  Synchronized Boolean Toggle
322
- 154.000 i/100ms
310
+ 158.000 i/100ms
323
311
  Concurrent Ruby Atomic Boolean Toggle
324
- 127.000 i/100ms
312
+ 113.000 i/100ms
325
313
  Atomic Ruby Atomic Boolean Toggle
326
- 139.000 i/100ms
314
+ 122.000 i/100ms
327
315
  Calculating -------------------------------------
328
316
  Synchronized Boolean Toggle
329
- 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
330
318
  Concurrent Ruby Atomic Boolean Toggle
331
- 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
332
320
  Atomic Ruby Atomic Boolean Toggle
333
- 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
334
322
 
335
323
  Comparison:
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
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
339
327
  ```
340
328
 
341
329
  </details>
@@ -353,14 +341,6 @@ require "benchmark"
353
341
  require "concurrent-ruby"
354
342
  require_relative "../lib/atomic-ruby"
355
343
 
356
- def shareable_proc(&block)
357
- if AtomicRuby::RACTOR_SAFE
358
- Ractor.shareable_proc(&block)
359
- else
360
- block
361
- end
362
- end
363
-
364
344
  results = []
365
345
 
366
346
  2.times do |idx|
@@ -371,11 +351,11 @@ results = []
371
351
  end
372
352
 
373
353
  100.times do
374
- pool << shareable_proc { sleep(0.2) }
354
+ pool << proc { sleep(0.2) }
375
355
  end
376
356
 
377
357
  100.times do
378
- pool << shareable_proc { 1_000_000.times.map(&:itself).sum }
358
+ pool << proc { 1_000_000.times.map(&:itself).sum }
379
359
  end
380
360
 
381
361
  pool.shutdown
@@ -399,13 +379,13 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
399
379
  ```
400
380
  > bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
401
381
 
402
- 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]
403
383
  concurrent-ruby version: 1.3.5
404
- atomic-ruby version: 0.8.0
384
+ atomic-ruby version: 0.9.0
405
385
 
406
386
  Benchmark Results:
407
- Concurrent Ruby Thread Pool: 5.13772 seconds
408
- Atomic Ruby Atomic Thread Pool: 4.893086 seconds
387
+ Concurrent Ruby Thread Pool: 5.169928 seconds
388
+ Atomic Ruby Atomic Thread Pool: 4.831942 seconds
409
389
  ```
410
390
 
411
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
@@ -38,10 +38,12 @@ module AtomicRuby
38
38
  def length
39
39
  @threads.select(&:alive?).length
40
40
  end
41
+ alias size length
41
42
 
42
43
  def queue_length
43
44
  @state.value[:queue].length
44
45
  end
46
+ alias queue_size queue_length
45
47
 
46
48
  def shutdown
47
49
  already_shutdown = false
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.8.0"
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.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young