atomic-ruby 0.2.0 → 0.3.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: afd2aac6df8cbc6d73dc79f925ab4f1d17288c3b1a146e2a52fea1ea1c343a30
4
- data.tar.gz: 77ed011cf73f079f07b09eb7d9f74d7db2da564bee8aa0e878d61241582b7fe6
3
+ metadata.gz: fc55b63bd9de85a395232f89184e97aa80e08e8e082981f2b17012fd1b56b5c0
4
+ data.tar.gz: 25cffc0cbb45f1d145d3b99380decc91a39ef82415833781bed40ba2583286a8
5
5
  SHA512:
6
- metadata.gz: 5053ea8e8d7158bf72fadf6d63b49a63a56550918df555237e1af42189bb2fddccc386d7ab485bbb1dbbb49cfe4633b827ff5c96ac94ab8e08f04da8c074e8b0
7
- data.tar.gz: 3eea3c132563bab777877c5e1785fe4ef4e11ae089c06aa4acd46d0db6465309de68ec728fdead66de0c51ca71ac8ce46d58dfc034407115582bb97f3010ecaa
6
+ metadata.gz: 1420ff50010eec4386301619d4866c1e96aec8f91936e464b2ed56276607fd6dfaf88d0cc5582ee255c06a15928a2b1390f1a2e18ceb443ee0d74fa190a36ee9
7
+ data.tar.gz: 80ceb6dd2348ea38bbd8b63223fc7ab27a6d36026d0e028fc0aef60dd9f92885b29adb5fa997b551b54c9aadbaca24fdd71a4f7b1d94337f3a3887955c108a5b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.1] - 2025-06-08
4
+
5
+ - Fix current queue being mutated in `AtomicThreadPool#<<`
6
+
7
+ ## [0.3.0] - 2025-06-08
8
+
9
+ - Add `AtomicBoolean`
10
+
3
11
  ## [0.2.0] - 2025-06-07
4
12
 
5
13
  - Add `AtomicThreadPool`
data/README.md CHANGED
@@ -27,11 +27,26 @@ gem install atomic-ruby
27
27
  require "atomic-ruby"
28
28
 
29
29
  atom = AtomicRuby::Atom.new(0)
30
- p atom.value # => 0
30
+ p atom.value #=> 0
31
31
  atom.swap { |current_value| current_value + 1 }
32
- p atom.value # => 1
32
+ p atom.value #=> 1
33
33
  atom.swap { |current_value| current_value + 1 }
34
- p atom.value # => 2
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
35
50
  ```
36
51
 
37
52
  `AtomicRuby::AtomicThreadPool`:
@@ -78,18 +93,22 @@ require "benchmark"
78
93
  require "concurrent-ruby"
79
94
  require_relative "../lib/atomic-ruby"
80
95
 
81
- class AtomicRubyAtomicBankAccount
96
+ class SynchronizedBankAccount
82
97
  def initialize(balance)
83
- @balance = AtomicRuby::Atom.new(balance)
98
+ @balance = balance
99
+ @mutex = Mutex.new
84
100
  end
85
101
 
86
102
  def balance
87
- @balance.value
103
+ @mutex.synchronize do
104
+ @balance
105
+ end
88
106
  end
89
107
 
90
108
  def deposit(amount)
91
- sleep(rand(0.1..0.2))
92
- @balance.swap { |current| current + amount }
109
+ @mutex.synchronize do
110
+ @balance += amount
111
+ end
93
112
  end
94
113
  end
95
114
 
@@ -103,51 +122,51 @@ class ConcurrentRubyAtomicBankAccount
103
122
  end
104
123
 
105
124
  def deposit(amount)
106
- sleep(rand(0.1..0.2))
107
- @balance.swap { |current| current + amount }
125
+ @balance.swap { |current_balance| current_balance + amount }
108
126
  end
109
127
  end
110
128
 
111
- class SynchronizedBankAccount
112
- attr_reader :balance
113
-
129
+ class AtomicRubyAtomicBankAccount
114
130
  def initialize(balance)
115
- @balance = balance
116
- @mutex = Mutex.new
131
+ @balance = AtomicRuby::Atom.new(balance)
132
+ end
133
+
134
+ def balance
135
+ @balance.value
117
136
  end
118
137
 
119
138
  def deposit(amount)
120
- sleep(rand(0.1..0.2))
121
- @mutex.synchronize do
122
- @balance += amount
123
- end
139
+ @balance.swap { |current_balance| current_balance + amount }
124
140
  end
125
141
  end
126
142
 
127
143
  balances = []
144
+ results = []
128
145
 
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
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
136
152
 
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
153
+ result = Benchmark.measure do
154
+ account = klass.new(100)
155
+
156
+ 5.times.map do |idx|
157
+ Thread.new do
158
+ 25.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
144
168
 
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
169
+ results << result
151
170
  end
152
171
 
153
172
  puts "ruby version: #{RUBY_DESCRIPTION}"
@@ -160,9 +179,9 @@ puts "Concurrent Ruby Atomic Bank Account Balance: #{balances[1]}"
160
179
  puts "Atomic Ruby Atomic Bank Account Balance: #{balances[2]}"
161
180
  puts "\n"
162
181
  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"
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"
166
185
  ```
167
186
 
168
187
  ```
@@ -170,17 +189,98 @@ puts "Atomic Ruby Atomic Bank Account: #{r3.real.round(6)} seconds"
170
189
 
171
190
  ruby version: ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
172
191
  concurrent-ruby version: 1.3.5
173
- atomic-ruby version: 0.1.0
192
+ atomic-ruby version: 0.3.0
174
193
 
175
194
  Balances:
176
- Synchronized Bank Account Balance: 49995100
177
- Concurrent Ruby Atomic Bank Account Balance: 49995100
178
- Atomic Ruby Atomic Bank Account Balance: 49995100
195
+ Synchronized Bank Account Balance: 975
196
+ Concurrent Ruby Atomic Bank Account Balance: 975
197
+ Atomic Ruby Atomic Bank Account Balance: 975
179
198
 
180
199
  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
200
+ Synchronized Bank Account: 5.125638 seconds
201
+ Concurrent Ruby Atomic Bank Account: 5.114936 seconds
202
+ Atomic Ruby Atomic Bank Account: 5.108171 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
184
284
  ```
185
285
 
186
286
  </details>
@@ -203,16 +303,16 @@ results = []
203
303
  2.times do |idx|
204
304
  result = Benchmark.measure do
205
305
  pool = case idx
206
- when 0 then Concurrent::FixedThreadPool.new(5)
207
- when 1 then AtomicRuby::AtomicThreadPool.new(size: 5)
306
+ when 0 then Concurrent::FixedThreadPool.new(20)
307
+ when 1 then AtomicRuby::AtomicThreadPool.new(size: 20)
208
308
  end
209
309
 
210
- 20.times do
211
- pool << -> { sleep(0.25) }
310
+ 100.times do
311
+ pool << -> { sleep(0.2) }
212
312
  end
213
313
 
214
- 20.times do
215
- pool << -> { 100_000.times.map(&:itself).sum }
314
+ 100.times do
315
+ pool << -> { 1_000_000.times.map(&:itself).sum }
216
316
  end
217
317
 
218
318
  # concurrent-ruby does not wait for threads to die on shutdown
@@ -240,11 +340,11 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
240
340
 
241
341
  ruby version: ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
242
342
  concurrent-ruby version: 1.3.5
243
- atomic-ruby version: 0.1.0
343
+ atomic-ruby version: 0.3.0
244
344
 
245
345
  Benchmark Results:
246
- Concurrent Ruby Thread Pool: 1.133100 seconds
247
- Atomic Ruby Atomic Thread Pool: 1.088543 seconds
346
+ Concurrent Ruby Thread Pool: 5.136456 seconds
347
+ Atomic Ruby Atomic Thread Pool: 4.700981 seconds
248
348
  ```
249
349
 
250
350
  </details>
@@ -48,6 +48,12 @@ static VALUE rb_cAtom_initialize(VALUE self, VALUE value) {
48
48
  return self;
49
49
  }
50
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
+
51
57
  static VALUE rb_cAtom_swap(VALUE self) {
52
58
  atomic_ruby_atom_t *atomic_ruby_atom;
53
59
  TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
@@ -61,18 +67,12 @@ static VALUE rb_cAtom_swap(VALUE self) {
61
67
  return new_value;
62
68
  }
63
69
 
64
- static VALUE rb_cAtom_value(VALUE self) {
65
- atomic_ruby_atom_t *atomic_ruby_atom;
66
- TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
67
- return (VALUE)RUBY_ATOMIC_PTR_LOAD(atomic_ruby_atom->value);
68
- }
69
-
70
70
  RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
71
71
  VALUE rb_mAtomicRuby = rb_define_module("AtomicRuby");
72
72
  VALUE rb_cAtom = rb_define_class_under(rb_mAtomicRuby, "Atom", rb_cObject);
73
73
 
74
74
  rb_define_alloc_func(rb_cAtom, rb_cAtom_allocate);
75
75
  rb_define_method(rb_cAtom, "_initialize", rb_cAtom_initialize, 1);
76
- rb_define_method(rb_cAtom, "_swap", rb_cAtom_swap, 0);
77
76
  rb_define_method(rb_cAtom, "_value", rb_cAtom_value, 0);
77
+ rb_define_method(rb_cAtom, "_swap", rb_cAtom_swap, 0);
78
78
  }
@@ -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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "atom"
4
+ require_relative "atomic_boolean"
4
5
 
5
6
  module AtomicRuby
6
7
  class AtomicThreadPool
@@ -12,21 +13,21 @@ module AtomicRuby
12
13
  @queue = Atom.new([])
13
14
  @threads = []
14
15
  @started_threads = Atom.new(0)
15
- @stopping = Atom.new(false)
16
+ @stopping = AtomicBoolean.new(false)
16
17
 
17
18
  start
18
19
  end
19
20
 
20
21
  def <<(work)
21
- unless Proc === work || work == :stop
22
- raise UnsupportedWorkTypeError, "queued work must be a Proc"
22
+ unless work.is_a?(Proc) || work == :stop
23
+ raise UnsupportedWorkTypeError, "expected work to be a `Proc`, got #{work.class}"
23
24
  end
24
25
 
25
- if @stopping.value
26
+ if @stopping.true?
26
27
  raise InvalidWorkQueueingError, "cannot queue work during or after pool shutdown"
27
28
  end
28
29
 
29
- @queue.swap { |queue| queue << work }
30
+ @queue.swap { |current_queue| current_queue += [work] }
30
31
  true
31
32
  end
32
33
 
@@ -52,11 +53,11 @@ module AtomicRuby
52
53
  name = "AtomicRuby::AtomicThreadPool thread #{idx}"
53
54
  Thread.current.name = name
54
55
 
55
- @started_threads.swap { |count| count + 1 }
56
+ @started_threads.swap { |current_count| current_count + 1 }
56
57
 
57
58
  loop do
58
59
  work = nil
59
- @queue.swap { |queue| work = queue.last; queue[0..-2] }
60
+ @queue.swap { |current_queue| work = current_queue.last; current_queue[0..-2] }
60
61
  case work
61
62
  when Proc
62
63
  begin
@@ -67,9 +68,9 @@ module AtomicRuby
67
68
  puts err.backtrace.join("\n")
68
69
  end
69
70
  when :stop
70
- @stopping.swap { true }
71
+ @stopping.make_true
71
72
  when NilClass
72
- if @stopping.value
73
+ if @stopping.true?
73
74
  break
74
75
  else
75
76
  Thread.pass
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.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.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young
@@ -26,6 +26,7 @@ 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
30
31
  - lib/atomic-ruby/atomic_thread_pool.rb
31
32
  - lib/atomic-ruby/version.rb