atomic-ruby 0.1.0 → 0.3.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: a3ca7bf11c2aa032352d7be8554faf13818bf1e982514ff60761779e2ec1283d
4
- data.tar.gz: 0b1965ea3c65e62d3c824e56ab673fc3e33f73f0283a39355aaf285f65a71b16
3
+ metadata.gz: 258cae699ce52593ad8958202446ff4ec5fffe8cd81771310d9a998dd10a3a09
4
+ data.tar.gz: 41af67fd11f30275d176691601f7f72863037583a0ecb70d3159da7d0e10e18a
5
5
  SHA512:
6
- metadata.gz: 9396b5a3a334cb5d89a17a09d59bf27ce1742b8d49720dae2c056a643ca1e0c5d32bbfded745c5557fd01b5fc420beaef3936540633adf090890f5b5a6a9b6c0
7
- data.tar.gz: 022cec086bc16c8b5437d42c9053c7fb2eca0484aea0e5d66221a292367efaebc0d4c30dbe12ad4d640f5751d7f4e9f9a3f90445c45aef58548d56d6e152c64e
6
+ metadata.gz: 3c49fea57c186b3f5b99f522d30aa94b5cf073b08e3b2179b58cd4da37daba3af5f8890b0abff209731e0bc4cdb350864b0d4a90e940cb4664763c4038b53f50
7
+ data.tar.gz: a4b0521b8a951ccb672e9f138983dc0f55f7fbaaf8a4848d17f9f88ff5dbfd6c4b46208252b45b0fec9cddb3432962711f961ef979d569b0d1962083c00706a6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-06-08
4
+
5
+ - Add `AtomicBoolean`
6
+
7
+ ## [0.2.0] - 2025-06-07
8
+
9
+ - Add `AtomicThreadPool`
10
+ - Require ruby >= 3.3
11
+ - Make `Atom#value` atomic
12
+
3
13
  ## [0.1.0] - 2025-06-06
4
14
 
5
15
  - Initial release
data/README.md CHANGED
@@ -1,36 +1,354 @@
1
1
  # AtomicRuby
2
2
 
3
+ ![Version](https://img.shields.io/gem/v/atomic-ruby)
4
+ ![Build](https://img.shields.io/github/actions/workflow/status/joshuay03/atomic-ruby/.github/workflows/main.yml?branch=main)
5
+
3
6
  Atomic primitives for Ruby.
4
7
 
5
8
  ## Installation
6
9
 
7
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
8
-
9
10
  Install the gem and add to the application's Gemfile by executing:
10
11
 
11
12
  ```bash
12
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
13
+ bundle add atomic-ruby
13
14
  ```
14
15
 
15
16
  If bundler is not being used to manage dependencies, install the gem by executing:
16
17
 
17
18
  ```bash
18
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
19
+ gem install atomic-ruby
19
20
  ```
20
21
 
21
22
  ## Usage
22
23
 
24
+ `AtomicRuby::Atom`:
25
+
23
26
  ```ruby
24
27
  require "atomic-ruby"
25
28
 
26
29
  atom = AtomicRuby::Atom.new(0)
27
- puts atom.value # => 0
28
- atom.swap { |current| current + 1 }
29
- puts atom.value # => 1
30
- atom.swap { |current| current + 1 }
31
- puts atom.value # => 2
30
+ p atom.value #=> 0
31
+ atom.swap { |current_value| current_value + 1 }
32
+ p atom.value #=> 1
33
+ atom.swap { |current_value| current_value + 1 }
34
+ p atom.value #=> 2
35
+ ```
36
+
37
+ `AtomicRuby::AtomicBoolean`:
38
+
39
+ ```ruby
40
+ require "atomic-ruby"
41
+
42
+ atom = AtomicRuby::AtomicBoolean.new(false)
43
+ p atom.value #=> false
44
+ p atom.false? #=> true
45
+ p atom.true? #=> false
46
+ atom.make_true
47
+ p atom.true? #=> true
48
+ atom.toggle
49
+ p atom.false? #=> true
50
+ ```
51
+
52
+ `AtomicRuby::AtomicThreadPool`:
53
+
54
+ ```ruby
55
+ require "atomic-ruby"
56
+
57
+ results = []
58
+
59
+ pool = AtomicRuby::AtomicThreadPool.new(size: 4)
60
+ p pool.length #=> 4
61
+
62
+ 10.times do |idx|
63
+ work = proc do
64
+ sleep(0.5)
65
+ results << (idx + 1)
66
+ end
67
+ pool << work
68
+ end
69
+ p pool.queue_length #=> 10
70
+ sleep(0.5)
71
+ p pool.queue_length #=> 2 (YMMV)
72
+
73
+ pool.shutdown
74
+ p pool.length #=> 0
75
+ p pool.queue_length #=> 0
76
+
77
+ p results #=> [8, 7, 10, 9, 6, 5, 3, 4, 2, 1]
78
+ p results.sort #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
79
+ ```
80
+
81
+ ## Benchmarks
82
+
83
+ <details>
84
+
85
+ <summary>AtomicRuby::Atom</summary>
86
+
87
+ <br>
88
+
89
+ ```ruby
90
+ # frozen_string_literal: true
91
+
92
+ require "benchmark"
93
+ require "concurrent-ruby"
94
+ require_relative "../lib/atomic-ruby"
95
+
96
+ class SynchronizedBankAccount
97
+ def initialize(balance)
98
+ @balance = balance
99
+ @mutex = Mutex.new
100
+ end
101
+
102
+ def balance
103
+ @mutex.synchronize do
104
+ @balance
105
+ end
106
+ end
107
+
108
+ def deposit(amount)
109
+ @mutex.synchronize do
110
+ @balance += amount
111
+ end
112
+ end
113
+ end
114
+
115
+ class ConcurrentRubyAtomicBankAccount
116
+ def initialize(balance)
117
+ @balance = Concurrent::Atom.new(balance)
118
+ end
119
+
120
+ def balance
121
+ @balance.value
122
+ end
123
+
124
+ def deposit(amount)
125
+ @balance.swap { |current_balance| current_balance + amount }
126
+ end
127
+ end
128
+
129
+ class AtomicRubyAtomicBankAccount
130
+ def initialize(balance)
131
+ @balance = AtomicRuby::Atom.new(balance)
132
+ end
133
+
134
+ def balance
135
+ @balance.value
136
+ end
137
+
138
+ def deposit(amount)
139
+ @balance.swap { |current_balance| current_balance + amount }
140
+ end
141
+ end
142
+
143
+ balances = []
144
+ results = []
145
+
146
+ 3.times do |idx|
147
+ klass = case idx
148
+ when 0 then SynchronizedBankAccount
149
+ when 1 then ConcurrentRubyAtomicBankAccount
150
+ when 2 then AtomicRubyAtomicBankAccount
151
+ end
152
+
153
+ result = Benchmark.measure do
154
+ account = klass.new(100)
155
+
156
+ 5.times.map do |idx|
157
+ Thread.new do
158
+ 100.times do
159
+ account.deposit(idx + 1)
160
+ sleep(0.2)
161
+ account.deposit(idx + 2)
162
+ end
163
+ end
164
+ end.each(&:join)
165
+
166
+ balances << account.balance
167
+ end
168
+
169
+ results << result
170
+ end
171
+
172
+ puts "ruby version: #{RUBY_DESCRIPTION}"
173
+ puts "concurrent-ruby version: #{Concurrent::VERSION}"
174
+ puts "atomic-ruby version: #{AtomicRuby::VERSION}"
175
+ puts "\n"
176
+ puts "Balances:"
177
+ puts "Synchronized Bank Account Balance: #{balances[0]}"
178
+ puts "Concurrent Ruby Atomic Bank Account Balance: #{balances[1]}"
179
+ puts "Atomic Ruby Atomic Bank Account Balance: #{balances[2]}"
180
+ puts "\n"
181
+ puts "Benchmark Results:"
182
+ puts "Synchronized Bank Account: #{results[0].real.round(6)} seconds"
183
+ puts "Concurrent Ruby Atomic Bank Account: #{results[1].real.round(6)} seconds"
184
+ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
32
185
  ```
33
186
 
187
+ ```
188
+ > bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
189
+
190
+ ruby version: ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
191
+ concurrent-ruby version: 1.3.5
192
+ atomic-ruby version: 0.2.0
193
+
194
+ Balances:
195
+ Synchronized Bank Account Balance: 3600
196
+ Concurrent Ruby Atomic Bank Account Balance: 3600
197
+ Atomic Ruby Atomic Bank Account Balance: 3600
198
+
199
+ Benchmark Results:
200
+ Synchronized Bank Account: 20.467293 seconds
201
+ Concurrent Ruby Atomic Bank Account: 20.460731 seconds
202
+ Atomic Ruby Atomic Bank Account: 20.455696 seconds
203
+ ```
204
+
205
+ </details>
206
+
207
+ <details>
208
+
209
+ <summary>AtomicRuby::AtomicBoolean</summary>
210
+
211
+ ```ruby
212
+ # frozen_string_literal: true
213
+
214
+ require "benchmark/ips"
215
+ require "concurrent-ruby"
216
+ require_relative "../lib/atomic-ruby"
217
+
218
+ Benchmark.ips do |x|
219
+ x.report("Synchronized Boolean Toggle") do
220
+ boolean = false
221
+ mutex = Mutex.new
222
+ 20.times.map do
223
+ Thread.new do
224
+ 100.times do
225
+ mutex.synchronize do
226
+ boolean = !boolean
227
+ end
228
+ end
229
+ end
230
+ end.each(&:join)
231
+ end
232
+
233
+ x.report("Concurrent Ruby Atomic Boolean Toggle") do
234
+ boolean = Concurrent::AtomicBoolean.new(false)
235
+ 20.times.map do
236
+ Thread.new do
237
+ 100.times do
238
+ # Not exactly atomic, but this
239
+ # is the closest matching API.
240
+ boolean.value = !boolean.value
241
+ end
242
+ end
243
+ end.each(&:join)
244
+ end
245
+
246
+ x.report("Atomic Ruby Atomic Boolean Toggle") do
247
+ boolean = AtomicRuby::AtomicBoolean.new(false)
248
+ 20.times.map do
249
+ Thread.new do
250
+ 100.times do
251
+ boolean.toggle
252
+ end
253
+ end
254
+ end.each(&:join)
255
+ end
256
+
257
+ x.compare!
258
+ end
259
+ ```
260
+
261
+ ```
262
+ > bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
263
+
264
+ ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
265
+ Warming up --------------------------------------
266
+ Synchronized Boolean Toggle
267
+ 83.000 i/100ms
268
+ Concurrent Ruby Atomic Boolean Toggle
269
+ 58.000 i/100ms
270
+ Atomic Ruby Atomic Boolean Toggle
271
+ 88.000 i/100ms
272
+ Calculating -------------------------------------
273
+ Synchronized Boolean Toggle
274
+ 775.552 (± 6.2%) i/s (1.29 ms/i) - 3.901k in 5.051649s
275
+ Concurrent Ruby Atomic Boolean Toggle
276
+ 741.655 (± 3.5%) i/s (1.35 ms/i) - 3.712k in 5.011183s
277
+ Atomic Ruby Atomic Boolean Toggle
278
+ 881.916 (± 2.8%) i/s (1.13 ms/i) - 4.488k in 5.092910s
279
+
280
+ Comparison:
281
+ Atomic Ruby Atomic Boolean Toggle: 881.9 i/s
282
+ Synchronized Boolean Toggle: 775.6 i/s - 1.14x slower
283
+ Concurrent Ruby Atomic Boolean Toggle: 741.7 i/s - 1.19x slower
284
+ ```
285
+
286
+ </details>
287
+
288
+ <details>
289
+
290
+ <summary>AtomicRuby::AtomicThreadPool</summary>
291
+
292
+ <br>
293
+
294
+ ```ruby
295
+ # frozen_string_literal: true
296
+
297
+ require "benchmark"
298
+ require "concurrent-ruby"
299
+ require_relative "../lib/atomic-ruby"
300
+
301
+ results = []
302
+
303
+ 2.times do |idx|
304
+ result = Benchmark.measure do
305
+ pool = case idx
306
+ when 0 then Concurrent::FixedThreadPool.new(20)
307
+ when 1 then AtomicRuby::AtomicThreadPool.new(size: 20)
308
+ end
309
+
310
+ 100.times do
311
+ pool << -> { sleep(0.2) }
312
+ end
313
+
314
+ 100.times do
315
+ pool << -> { 1_000_000.times.map(&:itself).sum }
316
+ end
317
+
318
+ # concurrent-ruby does not wait for threads to die on shutdown
319
+ threads = if idx == 0
320
+ pool.instance_variable_get(:@pool).map { |worker| worker.instance_variable_get(:@thread) }
321
+ end
322
+ pool.shutdown
323
+ threads&.each(&:join)
324
+ end
325
+
326
+ results << result
327
+ end
328
+
329
+ puts "ruby version: #{RUBY_DESCRIPTION}"
330
+ puts "concurrent-ruby version: #{Concurrent::VERSION}"
331
+ puts "atomic-ruby version: #{AtomicRuby::VERSION}"
332
+ puts "\n"
333
+ puts "Benchmark Results:"
334
+ puts "Concurrent Ruby Thread Pool: #{results[0].real.round(6)} seconds"
335
+ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
336
+ ```
337
+
338
+ ```
339
+ > bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
340
+
341
+ ruby version: ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
342
+ concurrent-ruby version: 1.3.5
343
+ atomic-ruby version: 0.2.0
344
+
345
+ Benchmark Results:
346
+ Concurrent Ruby Thread Pool: 5.188700 seconds
347
+ Atomic Ruby Atomic Thread Pool: 4.783689 seconds
348
+ ```
349
+
350
+ </details>
351
+
34
352
  ## Development
35
353
 
36
354
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests.
data/atomic-ruby.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.summary = "Atomic primitives for Ruby"
12
12
  spec.homepage = "https://github.com/joshuay03/atomic-ruby"
13
13
  spec.license = "MIT"
14
- spec.required_ruby_version = ">= 3.2.0"
14
+ spec.required_ruby_version = ">= 3.3.0"
15
15
 
16
16
  spec.metadata["source_code_uri"] = spec.homepage
17
17
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
@@ -1,6 +1,4 @@
1
1
  #include "atomic_ruby.h"
2
- #include <stdlib.h>
3
- #include <string.h>
4
2
 
5
3
  typedef struct {
6
4
  volatile VALUE value;
@@ -50,6 +48,12 @@ static VALUE rb_cAtom_initialize(VALUE self, VALUE value) {
50
48
  return self;
51
49
  }
52
50
 
51
+ static VALUE rb_cAtom_value(VALUE self) {
52
+ atomic_ruby_atom_t *atomic_ruby_atom;
53
+ TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
54
+ return (VALUE)RUBY_ATOMIC_PTR_LOAD(atomic_ruby_atom->value);
55
+ }
56
+
53
57
  static VALUE rb_cAtom_swap(VALUE self) {
54
58
  atomic_ruby_atom_t *atomic_ruby_atom;
55
59
  TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
@@ -63,18 +67,12 @@ static VALUE rb_cAtom_swap(VALUE self) {
63
67
  return new_value;
64
68
  }
65
69
 
66
- static VALUE rb_cAtom_value(VALUE self) {
67
- atomic_ruby_atom_t *atomic_ruby_atom;
68
- TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
69
- return atomic_ruby_atom->value;
70
- }
71
-
72
70
  RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
73
71
  VALUE rb_mAtomicRuby = rb_define_module("AtomicRuby");
74
72
  VALUE rb_cAtom = rb_define_class_under(rb_mAtomicRuby, "Atom", rb_cObject);
75
73
 
76
74
  rb_define_alloc_func(rb_cAtom, rb_cAtom_allocate);
77
75
  rb_define_method(rb_cAtom, "_initialize", rb_cAtom_initialize, 1);
78
- rb_define_method(rb_cAtom, "swap", rb_cAtom_swap, 0);
79
- rb_define_method(rb_cAtom, "value", rb_cAtom_value, 0);
76
+ rb_define_method(rb_cAtom, "_value", rb_cAtom_value, 0);
77
+ rb_define_method(rb_cAtom, "_swap", rb_cAtom_swap, 0);
80
78
  }
@@ -5,5 +5,13 @@ module AtomicRuby
5
5
  def initialize(value)
6
6
  _initialize(value)
7
7
  end
8
+
9
+ def value
10
+ _value
11
+ end
12
+
13
+ def swap(&block)
14
+ _swap(&block)
15
+ end
8
16
  end
9
17
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "atom"
4
+
5
+ module AtomicRuby
6
+ class AtomicBoolean
7
+ class InvalidBooleanError < StandardError; end
8
+
9
+ def initialize(value)
10
+ unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
11
+ raise InvalidBooleanError, "expected boolean to be a `TrueClass` or `FalseClass`, got #{value.class}"
12
+ end
13
+
14
+ @atom = Atom.new(value)
15
+ end
16
+
17
+ def value
18
+ @atom.value
19
+ end
20
+
21
+ def true?
22
+ value == true
23
+ end
24
+
25
+ def false?
26
+ value == false
27
+ end
28
+
29
+ def make_true
30
+ @atom.swap { true }
31
+ end
32
+
33
+ def make_false
34
+ @atom.swap { false }
35
+ end
36
+
37
+ def toggle
38
+ @atom.swap { |current_value| !current_value }
39
+ end
40
+ end
41
+ end
Binary file
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "atom"
4
+ require_relative "atomic_boolean"
5
+
6
+ module AtomicRuby
7
+ class AtomicThreadPool
8
+ class UnsupportedWorkTypeError < StandardError; end
9
+ class InvalidWorkQueueingError < StandardError; end
10
+
11
+ def initialize(size:)
12
+ @size = size
13
+ @queue = Atom.new([])
14
+ @threads = []
15
+ @started_threads = Atom.new(0)
16
+ @stopping = AtomicBoolean.new(false)
17
+
18
+ start
19
+ end
20
+
21
+ def <<(work)
22
+ unless work.is_a?(Proc) || work == :stop
23
+ raise UnsupportedWorkTypeError, "expected work to be a `Proc`, got #{work.class}"
24
+ end
25
+
26
+ if @stopping.true?
27
+ raise InvalidWorkQueueingError, "cannot queue work during or after pool shutdown"
28
+ end
29
+
30
+ @queue.swap { |queue| queue << work }
31
+ true
32
+ end
33
+
34
+ def length
35
+ @threads.select(&:alive?).length
36
+ end
37
+
38
+ def queue_length
39
+ @queue.value.length
40
+ end
41
+
42
+ def shutdown
43
+ self << :stop
44
+ @threads.each(&:join)
45
+ true
46
+ end
47
+
48
+ private
49
+
50
+ def start
51
+ @threads = @size.times.map do |num|
52
+ Thread.new(num) do |idx|
53
+ name = "AtomicRuby::AtomicThreadPool thread #{idx}"
54
+ Thread.current.name = name
55
+
56
+ @started_threads.swap { |count| count + 1 }
57
+
58
+ loop do
59
+ work = nil
60
+ @queue.swap { |queue| work = queue.last; queue[0..-2] }
61
+ case work
62
+ when Proc
63
+ begin
64
+ work.call
65
+ rescue => err
66
+ puts "#{name} rescued:"
67
+ puts "#{err.class}: #{err.message}"
68
+ puts err.backtrace.join("\n")
69
+ end
70
+ when :stop
71
+ @stopping.make_true
72
+ when NilClass
73
+ if @stopping.true?
74
+ break
75
+ else
76
+ Thread.pass
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ sleep(0.001) until @started_threads.value == @size
84
+ end
85
+ end
86
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/atomic-ruby.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "atomic-ruby/version"
4
4
  require_relative "atomic-ruby/atomic_ruby"
5
5
  require_relative "atomic-ruby/atom"
6
+ require_relative "atomic-ruby/atomic_thread_pool"
6
7
 
7
8
  module AtomicRuby
8
9
  class Error < StandardError; 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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young
@@ -26,7 +26,9 @@ files:
26
26
  - ext/atomic_ruby/extconf.rb
27
27
  - lib/atomic-ruby.rb
28
28
  - lib/atomic-ruby/atom.rb
29
+ - lib/atomic-ruby/atomic_boolean.rb
29
30
  - lib/atomic-ruby/atomic_ruby.bundle
31
+ - lib/atomic-ruby/atomic_thread_pool.rb
30
32
  - lib/atomic-ruby/version.rb
31
33
  homepage: https://github.com/joshuay03/atomic-ruby
32
34
  licenses:
@@ -41,7 +43,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
41
43
  requirements:
42
44
  - - ">="
43
45
  - !ruby/object:Gem::Version
44
- version: 3.2.0
46
+ version: 3.3.0
45
47
  required_rubygems_version: !ruby/object:Gem::Requirement
46
48
  requirements:
47
49
  - - ">="