atomic-ruby 0.1.0 → 0.2.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: afd2aac6df8cbc6d73dc79f925ab4f1d17288c3b1a146e2a52fea1ea1c343a30
4
+ data.tar.gz: 77ed011cf73f079f07b09eb7d9f74d7db2da564bee8aa0e878d61241582b7fe6
5
5
  SHA512:
6
- metadata.gz: 9396b5a3a334cb5d89a17a09d59bf27ce1742b8d49720dae2c056a643ca1e0c5d32bbfded745c5557fd01b5fc420beaef3936540633adf090890f5b5a6a9b6c0
7
- data.tar.gz: 022cec086bc16c8b5437d42c9053c7fb2eca0484aea0e5d66221a292367efaebc0d4c30dbe12ad4d640f5751d7f4e9f9a3f90445c45aef58548d56d6e152c64e
6
+ metadata.gz: 5053ea8e8d7158bf72fadf6d63b49a63a56550918df555237e1af42189bb2fddccc386d7ab485bbb1dbbb49cfe4633b827ff5c96ac94ab8e08f04da8c074e8b0
7
+ data.tar.gz: 3eea3c132563bab777877c5e1785fe4ef4e11ae089c06aa4acd46d0db6465309de68ec728fdead66de0c51ca71ac8ce46d58dfc034407115582bb97f3010ecaa
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-06-07
4
+
5
+ - Add `AtomicThreadPool`
6
+ - Require ruby >= 3.3
7
+ - Make `Atom#value` atomic
8
+
3
9
  ## [0.1.0] - 2025-06-06
4
10
 
5
11
  - Initial release
data/README.md CHANGED
@@ -1,36 +1,254 @@
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::AtomicThreadPool`:
38
+
39
+ ```ruby
40
+ require "atomic-ruby"
41
+
42
+ results = []
43
+
44
+ pool = AtomicRuby::AtomicThreadPool.new(size: 4)
45
+ p pool.length #=> 4
46
+
47
+ 10.times do |idx|
48
+ work = proc do
49
+ sleep(0.5)
50
+ results << (idx + 1)
51
+ end
52
+ pool << work
53
+ end
54
+ p pool.queue_length #=> 10
55
+ sleep(0.5)
56
+ p pool.queue_length #=> 2 (YMMV)
57
+
58
+ pool.shutdown
59
+ p pool.length #=> 0
60
+ p pool.queue_length #=> 0
61
+
62
+ p results #=> [8, 7, 10, 9, 6, 5, 3, 4, 2, 1]
63
+ p results.sort #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
64
+ ```
65
+
66
+ ## Benchmarks
67
+
68
+ <details>
69
+
70
+ <summary>AtomicRuby::Atom</summary>
71
+
72
+ <br>
73
+
74
+ ```ruby
75
+ # frozen_string_literal: true
76
+
77
+ require "benchmark"
78
+ require "concurrent-ruby"
79
+ require_relative "../lib/atomic-ruby"
80
+
81
+ class AtomicRubyAtomicBankAccount
82
+ def initialize(balance)
83
+ @balance = AtomicRuby::Atom.new(balance)
84
+ end
85
+
86
+ def balance
87
+ @balance.value
88
+ end
89
+
90
+ def deposit(amount)
91
+ sleep(rand(0.1..0.2))
92
+ @balance.swap { |current| current + amount }
93
+ end
94
+ end
95
+
96
+ class ConcurrentRubyAtomicBankAccount
97
+ def initialize(balance)
98
+ @balance = Concurrent::Atom.new(balance)
99
+ end
100
+
101
+ def balance
102
+ @balance.value
103
+ end
104
+
105
+ def deposit(amount)
106
+ sleep(rand(0.1..0.2))
107
+ @balance.swap { |current| current + amount }
108
+ end
109
+ end
110
+
111
+ class SynchronizedBankAccount
112
+ attr_reader :balance
113
+
114
+ def initialize(balance)
115
+ @balance = balance
116
+ @mutex = Mutex.new
117
+ end
118
+
119
+ def deposit(amount)
120
+ sleep(rand(0.1..0.2))
121
+ @mutex.synchronize do
122
+ @balance += amount
123
+ end
124
+ end
125
+ end
126
+
127
+ balances = []
128
+
129
+ r1 = Benchmark.measure do
130
+ account = SynchronizedBankAccount.new(100)
131
+ 10_000.times.map { |i|
132
+ Thread.new { account.deposit(i) }
133
+ }.each(&:join)
134
+ balances << account.balance
135
+ end
136
+
137
+ r2 = Benchmark.measure do
138
+ account = ConcurrentRubyAtomicBankAccount.new(100)
139
+ 10_000.times.map { |i|
140
+ Thread.new { account.deposit(i) }
141
+ }.each(&:join)
142
+ balances << account.balance
143
+ end
144
+
145
+ r3 = Benchmark.measure do
146
+ account = AtomicRubyAtomicBankAccount.new(100)
147
+ 10_000.times.map { |i|
148
+ Thread.new { account.deposit(i) }
149
+ }.each(&:join)
150
+ balances << account.balance
151
+ end
152
+
153
+ puts "ruby version: #{RUBY_DESCRIPTION}"
154
+ puts "concurrent-ruby version: #{Concurrent::VERSION}"
155
+ puts "atomic-ruby version: #{AtomicRuby::VERSION}"
156
+ puts "\n"
157
+ puts "Balances:"
158
+ puts "Synchronized Bank Account Balance: #{balances[0]}"
159
+ puts "Concurrent Ruby Atomic Bank Account Balance: #{balances[1]}"
160
+ puts "Atomic Ruby Atomic Bank Account Balance: #{balances[2]}"
161
+ puts "\n"
162
+ puts "Benchmark Results:"
163
+ puts "Synchronized Bank Account: #{r1.real.round(6)} seconds"
164
+ puts "Concurrent Ruby Atomic Bank Account: #{r2.real.round(6)} seconds"
165
+ puts "Atomic Ruby Atomic Bank Account: #{r3.real.round(6)} seconds"
166
+ ```
167
+
168
+ ```
169
+ > bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
170
+
171
+ ruby version: ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
172
+ concurrent-ruby version: 1.3.5
173
+ atomic-ruby version: 0.1.0
174
+
175
+ Balances:
176
+ Synchronized Bank Account Balance: 49995100
177
+ Concurrent Ruby Atomic Bank Account Balance: 49995100
178
+ Atomic Ruby Atomic Bank Account Balance: 49995100
179
+
180
+ Benchmark Results:
181
+ Synchronized Bank Account: 1.900873 seconds
182
+ Concurrent Ruby Atomic Bank Account: 1.840683 seconds
183
+ Atomic Ruby Atomic Bank Account: 1.755343 seconds
32
184
  ```
33
185
 
186
+ </details>
187
+
188
+ <details>
189
+
190
+ <summary>AtomicRuby::AtomicThreadPool</summary>
191
+
192
+ <br>
193
+
194
+ ```ruby
195
+ # frozen_string_literal: true
196
+
197
+ require "benchmark"
198
+ require "concurrent-ruby"
199
+ require_relative "../lib/atomic-ruby"
200
+
201
+ results = []
202
+
203
+ 2.times do |idx|
204
+ result = Benchmark.measure do
205
+ pool = case idx
206
+ when 0 then Concurrent::FixedThreadPool.new(5)
207
+ when 1 then AtomicRuby::AtomicThreadPool.new(size: 5)
208
+ end
209
+
210
+ 20.times do
211
+ pool << -> { sleep(0.25) }
212
+ end
213
+
214
+ 20.times do
215
+ pool << -> { 100_000.times.map(&:itself).sum }
216
+ end
217
+
218
+ # concurrent-ruby does not wait for threads to die on shutdown
219
+ threads = if idx == 0
220
+ pool.instance_variable_get(:@pool).map { |worker| worker.instance_variable_get(:@thread) }
221
+ end
222
+ pool.shutdown
223
+ threads&.each(&:join)
224
+ end
225
+
226
+ results << result
227
+ end
228
+
229
+ puts "ruby version: #{RUBY_DESCRIPTION}"
230
+ puts "concurrent-ruby version: #{Concurrent::VERSION}"
231
+ puts "atomic-ruby version: #{AtomicRuby::VERSION}"
232
+ puts "\n"
233
+ puts "Benchmark Results:"
234
+ puts "Concurrent Ruby Thread Pool: #{results[0].real.round(6)} seconds"
235
+ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
236
+ ```
237
+
238
+ ```
239
+ > bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
240
+
241
+ ruby version: ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
242
+ concurrent-ruby version: 1.3.5
243
+ atomic-ruby version: 0.1.0
244
+
245
+ Benchmark Results:
246
+ Concurrent Ruby Thread Pool: 1.133100 seconds
247
+ Atomic Ruby Atomic Thread Pool: 1.088543 seconds
248
+ ```
249
+
250
+ </details>
251
+
34
252
  ## Development
35
253
 
36
254
  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;
@@ -66,7 +64,7 @@ static VALUE rb_cAtom_swap(VALUE self) {
66
64
  static VALUE rb_cAtom_value(VALUE self) {
67
65
  atomic_ruby_atom_t *atomic_ruby_atom;
68
66
  TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
69
- return atomic_ruby_atom->value;
67
+ return (VALUE)RUBY_ATOMIC_PTR_LOAD(atomic_ruby_atom->value);
70
68
  }
71
69
 
72
70
  RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
@@ -75,6 +73,6 @@ RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
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, "_swap", rb_cAtom_swap, 0);
77
+ rb_define_method(rb_cAtom, "_value", rb_cAtom_value, 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
Binary file
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "atom"
4
+
5
+ module AtomicRuby
6
+ class AtomicThreadPool
7
+ class UnsupportedWorkTypeError < StandardError; end
8
+ class InvalidWorkQueueingError < StandardError; end
9
+
10
+ def initialize(size:)
11
+ @size = size
12
+ @queue = Atom.new([])
13
+ @threads = []
14
+ @started_threads = Atom.new(0)
15
+ @stopping = Atom.new(false)
16
+
17
+ start
18
+ end
19
+
20
+ def <<(work)
21
+ unless Proc === work || work == :stop
22
+ raise UnsupportedWorkTypeError, "queued work must be a Proc"
23
+ end
24
+
25
+ if @stopping.value
26
+ raise InvalidWorkQueueingError, "cannot queue work during or after pool shutdown"
27
+ end
28
+
29
+ @queue.swap { |queue| queue << work }
30
+ true
31
+ end
32
+
33
+ def length
34
+ @threads.select(&:alive?).length
35
+ end
36
+
37
+ def queue_length
38
+ @queue.value.length
39
+ end
40
+
41
+ def shutdown
42
+ self << :stop
43
+ @threads.each(&:join)
44
+ true
45
+ end
46
+
47
+ private
48
+
49
+ def start
50
+ @threads = @size.times.map do |num|
51
+ Thread.new(num) do |idx|
52
+ name = "AtomicRuby::AtomicThreadPool thread #{idx}"
53
+ Thread.current.name = name
54
+
55
+ @started_threads.swap { |count| count + 1 }
56
+
57
+ loop do
58
+ work = nil
59
+ @queue.swap { |queue| work = queue.last; queue[0..-2] }
60
+ case work
61
+ when Proc
62
+ begin
63
+ work.call
64
+ rescue => err
65
+ puts "#{name} rescued:"
66
+ puts "#{err.class}: #{err.message}"
67
+ puts err.backtrace.join("\n")
68
+ end
69
+ when :stop
70
+ @stopping.swap { true }
71
+ when NilClass
72
+ if @stopping.value
73
+ break
74
+ else
75
+ Thread.pass
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ sleep(0.001) until @started_threads.value == @size
83
+ end
84
+ end
85
+ 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.2.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.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young
@@ -27,6 +27,7 @@ files:
27
27
  - lib/atomic-ruby.rb
28
28
  - lib/atomic-ruby/atom.rb
29
29
  - lib/atomic-ruby/atomic_ruby.bundle
30
+ - lib/atomic-ruby/atomic_thread_pool.rb
30
31
  - lib/atomic-ruby/version.rb
31
32
  homepage: https://github.com/joshuay03/atomic-ruby
32
33
  licenses:
@@ -41,7 +42,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
41
42
  requirements:
42
43
  - - ">="
43
44
  - !ruby/object:Gem::Version
44
- version: 3.2.0
45
+ version: 3.3.0
45
46
  required_rubygems_version: !ruby/object:Gem::Requirement
46
47
  requirements:
47
48
  - - ">="