atomic-ruby 0.10.0 → 0.11.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: b52931f98d46947d0d2f4dccffbe51e771c619d61b54457a223162be41624353
4
- data.tar.gz: 3c91a27d99c3eb4d2a04f668bc5ebbc19c1c60b6a9fc688812df96c20bed37a4
3
+ metadata.gz: 4a9ae1ba9062331be3f6a5d74f170dbf88eb2ce6b142f3ef5077194ddeb1295e
4
+ data.tar.gz: 652fbf6d04d6efa3d57fc37643f85241e685950320d343e2341c155b0539bf93
5
5
  SHA512:
6
- metadata.gz: b3f6d04175439390f6b9f29d91bf3cfad5b95018f7363e3cb341c7512857008e285d3a462363108cb54e613ed898153e157d7c41b1e113d92c38053a52e5d5be
7
- data.tar.gz: aadb6f2b22fa068d8599b1e0aea30bc6e52d4b97198c4e4078e465d172f168b8548c9ab2f4bd174298aa2f5d02cd421c868a896c4b0d53689706edeb2414ff35
6
+ metadata.gz: 686a5aaeef7250102c57a7b5b9b2dbe5cdbe45105428a98217545d6c3b1183ef79696f64b5a7aa1135a0f260bdcb8c50b53b4455dc60785977687d62b8b3c47a
7
+ data.tar.gz: 2141ed20f4baf5e2f8cf175c69b8003bf62413e1e312b5b8bf402ea762d409bcbfc72bfc14955d6aa4319ee719c6d28880055e954d2bb69278f9154d71b5a970
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.11.1] - 2026-05-17
4
+
5
+ - Replace `Thread.pass` busy-wait with `sleep` in idle `AtomicThreadPool` workers
6
+
7
+ ## [0.11.0] - 2026-05-10
8
+
9
+ - Use `warn` in `AtomicThreadPool` error output
10
+ - Add `on_error:` to `AtomicThreadPool`
11
+ - Remove redundant `Thread.pass` spin in `AtomicThreadPool#shutdown`
12
+ - Fix O(n) performance issue in `AtomicThreadPool` queue by using two-stack linked list
13
+
3
14
  ## [0.10.0] - 2025-11-09
4
15
 
5
16
  - Add `AtomicThreadPool#active_count`
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # AtomicRuby
2
2
 
3
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)
4
+ ![Build](https://badge.buildkite.com/42198db99cf0eb852d54a6f125d99e68a5c0cd2e1d63026913.svg)
5
5
 
6
6
  Atomic ([CAS](https://en.wikipedia.org/wiki/Compare-and-swap)) primitives for Ruby.
7
7
 
@@ -210,9 +210,9 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
210
210
  ```
211
211
  > bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
212
212
 
213
- ruby version: ruby 4.0.0dev (2025-11-08T15:08:09Z master 75d25a42e6) +YJIT +PRISM [arm64-darwin25]
214
- concurrent-ruby version: 1.3.5
215
- atomic-ruby version: 0.10.0
213
+ ruby version: ruby 4.0.3 (2026-04-21 revision 85ddef263a) +YJIT +PRISM [arm64-darwin23]
214
+ concurrent-ruby version: 1.3.6
215
+ atomic-ruby version: 0.11.0
216
216
 
217
217
  Balances:
218
218
  Synchronized Bank Account Balance: 975
@@ -220,9 +220,9 @@ Concurrent Ruby Atomic Bank Account Balance: 975
220
220
  Atomic Ruby Atomic Bank Account Balance: 975
221
221
 
222
222
  Benchmark Results:
223
- Synchronized Bank Account: 5.112382 seconds
224
- Concurrent Ruby Atomic Bank Account: 5.113139 seconds
225
- Atomic Ruby Atomic Bank Account: 5.101891 seconds
223
+ Synchronized Bank Account: 5.106931 seconds
224
+ Concurrent Ruby Atomic Bank Account: 5.106199 seconds
225
+ Atomic Ruby Atomic Bank Account: 5.101662 seconds
226
226
  ```
227
227
 
228
228
  </details>
@@ -301,29 +301,29 @@ end
301
301
  ```
302
302
  > bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
303
303
 
304
- ruby version: ruby 4.0.0dev (2025-11-08T15:08:09Z master 75d25a42e6) +YJIT +PRISM [arm64-darwin25]
305
- concurrent-ruby version: 1.3.5
306
- atomic-ruby version: 0.10.0
304
+ ruby version: ruby 4.0.3 (2026-04-21 revision 85ddef263a) +YJIT +PRISM [arm64-darwin23]
305
+ concurrent-ruby version: 1.3.6
306
+ atomic-ruby version: 0.11.0
307
307
 
308
308
  Warming up --------------------------------------
309
309
  Synchronized Boolean Toggle
310
- 120.000 i/100ms
310
+ 135.000 i/100ms
311
311
  Concurrent Ruby Atomic Boolean Toggle
312
- 94.000 i/100ms
312
+ 117.000 i/100ms
313
313
  Atomic Ruby Atomic Boolean Toggle
314
- 100.000 i/100ms
314
+ 101.000 i/100ms
315
315
  Calculating -------------------------------------
316
316
  Synchronized Boolean Toggle
317
- 1.188k8.9%) i/s (841.70 μs/i) - 5.880k in 5.002927s
317
+ 1.414k1.4%) i/s (707.15 μs/i) - 7.155k in 5.060692s
318
318
  Concurrent Ruby Atomic Boolean Toggle
319
- 889.22411.8%) i/s (1.12 ms/i) - 4.418k in 5.073535s
319
+ 1.149k 2.9%) i/s (870.53 μs/i) - 5.850k in 5.097019s
320
320
  Atomic Ruby Atomic Boolean Toggle
321
- 999.4264.3%) i/s (1.00 ms/i) - 5.000k in 5.012997s
321
+ 1.046k2.1%) i/s (955.57 μs/i) - 5.252k in 5.021118s
322
322
 
323
323
  Comparison:
324
- Synchronized Boolean Toggle: 1188.1 i/s
325
- Atomic Ruby Atomic Boolean Toggle: 999.4 i/s - 1.19x slower
326
- Concurrent Ruby Atomic Boolean Toggle: 889.2 i/s - 1.34x slower
324
+ Synchronized Boolean Toggle: 1414.1 i/s
325
+ Concurrent Ruby Atomic Boolean Toggle: 1148.7 i/s - 1.23x slower
326
+ Atomic Ruby Atomic Boolean Toggle: 1046.5 i/s - 1.35x slower
327
327
  ```
328
328
 
329
329
  </details>
@@ -379,13 +379,13 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
379
379
  ```
380
380
  > bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
381
381
 
382
- ruby version: ruby 4.0.0dev (2025-11-08T15:08:09Z master 75d25a42e6) +YJIT +PRISM [arm64-darwin25]
383
- concurrent-ruby version: 1.3.5
384
- atomic-ruby version: 0.10.0
382
+ ruby version: ruby 4.0.4 (2026-05-12 revision b89eb1bcbf) +YJIT +PRISM [arm64-darwin23]
383
+ concurrent-ruby version: 1.3.6
384
+ atomic-ruby version: 0.11.1
385
385
 
386
386
  Benchmark Results:
387
- Concurrent Ruby Thread Pool: 5.56943 seconds
388
- Atomic Ruby Atomic Thread Pool: 5.252876 seconds
387
+ Concurrent Ruby Thread Pool: 5.869825 seconds
388
+ Atomic Ruby Atomic Thread Pool: 5.387304 seconds
389
389
  ```
390
390
 
391
391
  </details>
@@ -10,6 +10,11 @@ module AtomicRuby
10
10
  # work items from an atomic queue. The pool uses compare-and-swap operations
11
11
  # for thread-safe work enqueueing and state management.
12
12
  #
13
+ # The queue is implemented as a two-stack structure (in/out) backed by
14
+ # immutable frozen linked-list nodes. Enqueueing prepends to the `in` stack
15
+ # in O(1); dequeueing pops from the `out` stack in O(1), reversing `in` into
16
+ # `out` only when `out` is exhausted (amortized O(1) per item).
17
+ #
13
18
  # @example Basic usage
14
19
  # pool = AtomicThreadPool.new(size: 4)
15
20
  # pool << proc { puts "Hello from worker thread!" }
@@ -52,9 +57,13 @@ module AtomicRuby
52
57
  #
53
58
  # @param size [Integer] The number of worker threads to create (must be positive)
54
59
  # @param name [String, nil] Optional name for the thread pool (used in thread names)
60
+ # @param on_error [Proc, nil] Optional error handler called with the exception when
61
+ # a work item raises. Receives the exception as its argument. When nil, errors
62
+ # are printed to stderr
55
63
  #
56
64
  # @raise [ArgumentError] if size is not a positive integer
57
65
  # @raise [ArgumentError] if name is provided but not a string
66
+ # @raise [ArgumentError] if on_error is provided but not a Proc
58
67
  #
59
68
  # @example Create a basic pool
60
69
  # pool = AtomicThreadPool.new(size: 4)
@@ -62,15 +71,21 @@ module AtomicRuby
62
71
  # @example Create a named pool
63
72
  # pool = AtomicThreadPool.new(size: 2, name: "Database Workers")
64
73
  #
65
- # @rbs (size: Integer, ?name: String?) -> void
66
- def initialize(size:, name: nil)
74
+ # @example Create a pool with a custom error handler
75
+ # errors = []
76
+ # pool = AtomicThreadPool.new(size: 2, on_error: ->(err) { errors << err })
77
+ #
78
+ # @rbs (size: Integer, ?name: String?, ?on_error: Proc?) -> void
79
+ def initialize(size:, name: nil, on_error: nil)
67
80
  raise ArgumentError, "size must be a positive Integer" unless size.is_a?(Integer) && size > 0
68
81
  raise ArgumentError, "name must be a String" unless name.nil? || name.is_a?(String)
82
+ raise ArgumentError, "on_error must be a Proc" unless on_error.nil? || on_error.is_a?(Proc)
69
83
 
70
84
  @size = size
71
85
  @name = name
86
+ @on_error = on_error
72
87
 
73
- @state = Atom.new(queue: [], shutdown: false)
88
+ @state = Atom.new(in: nil, out: nil, count: 0, shutdown: false)
74
89
  @started_thread_count = Atom.new(0)
75
90
  @active_thread_count = Atom.new(0)
76
91
  @threads = []
@@ -83,6 +98,7 @@ module AtomicRuby
83
98
  # The work item must respond to #call (typically a Proc or lambda).
84
99
  # Work items are executed in FIFO order by available worker threads.
85
100
  # If all workers are busy, the work is queued atomically.
101
+ # Enqueueing is O(1) regardless of current queue depth.
86
102
  #
87
103
  # @param work [#call] A callable object to be executed by a worker thread
88
104
  #
@@ -105,7 +121,8 @@ module AtomicRuby
105
121
  if current_state[:shutdown]
106
122
  current_state
107
123
  else
108
- current_state.merge(queue: [*current_state[:queue], work])
124
+ new_node = { value: work, next: current_state[:in] }.freeze
125
+ current_state.merge(in: new_node, count: current_state[:count] + 1)
109
126
  end
110
127
  end
111
128
  raise EnqueuedWorkAfterShutdownError if state[:shutdown]
@@ -130,6 +147,7 @@ module AtomicRuby
130
147
  @threads.select(&:alive?).length
131
148
  end
132
149
  # Alias for {#length}.
150
+ #
133
151
  # @rbs () -> Integer
134
152
  alias size length
135
153
 
@@ -148,9 +166,10 @@ module AtomicRuby
148
166
  #
149
167
  # @rbs () -> Integer
150
168
  def queue_length
151
- @state.value[:queue].length
169
+ @state.value[:count]
152
170
  end
153
171
  # Alias for {#queue_length}.
172
+ #
154
173
  # @rbs () -> Integer
155
174
  alias queue_size queue_length
156
175
 
@@ -215,8 +234,6 @@ module AtomicRuby
215
234
  end
216
235
  return if already_shutdown
217
236
 
218
- Thread.pass until @state.value[:queue].empty?
219
-
220
237
  @threads.each(&:join)
221
238
  end
222
239
 
@@ -229,6 +246,7 @@ module AtomicRuby
229
246
  # for all threads to be fully started before returning.
230
247
  #
231
248
  # @return [void]
249
+ #
232
250
  # @rbs () -> void
233
251
  def start
234
252
  @size.times do |num|
@@ -244,14 +262,18 @@ module AtomicRuby
244
262
  should_shutdown = false
245
263
 
246
264
  @state.swap do |current_state|
247
- if current_state[:shutdown] && current_state[:queue].empty?
265
+ if current_state[:shutdown] && current_state[:in].nil? && current_state[:out].nil?
248
266
  should_shutdown = true
249
267
  current_state
250
- elsif current_state[:queue].empty?
251
- current_state
268
+ elsif current_state[:out]
269
+ work = current_state[:out][:value]
270
+ current_state.merge(out: current_state[:out][:next], count: current_state[:count] - 1)
271
+ elsif current_state[:in]
272
+ new_out = reverse_list(current_state[:in])
273
+ work = new_out[:value]
274
+ current_state.merge(in: nil, out: new_out[:next], count: current_state[:count] - 1)
252
275
  else
253
- work = current_state[:queue].first
254
- current_state.merge(queue: current_state[:queue].drop(1))
276
+ current_state
255
277
  end
256
278
  end
257
279
 
@@ -262,14 +284,17 @@ module AtomicRuby
262
284
  begin
263
285
  work.call
264
286
  rescue => err
265
- puts "#{thread_name} rescued:"
266
- puts "#{err.class}: #{err.message}"
267
- puts err.backtrace.join("\n")
287
+ if @on_error
288
+ @on_error.call(err)
289
+ else
290
+ warn "#{thread_name} rescued:"
291
+ warn err.full_message
292
+ end
268
293
  ensure
269
294
  @active_thread_count.swap { |current_count| current_count - 1 }
270
295
  end
271
296
  else
272
- Thread.pass
297
+ sleep 0.001
273
298
  end
274
299
  end
275
300
  end
@@ -278,6 +303,22 @@ module AtomicRuby
278
303
 
279
304
  Thread.pass until @started_thread_count.value == @size
280
305
  end
306
+
307
+ # Reverses a linked-list of frozen nodes, returning a new reversed list.
308
+ # Each node in the returned list is a new frozen hash.
309
+ #
310
+ # @param node [Hash, nil] Head of the list to reverse
311
+ # @return [Hash, nil] Head of the reversed list
312
+ #
313
+ # @rbs (Hash? node) -> Hash?
314
+ def reverse_list(node)
315
+ reversed = nil
316
+ while node
317
+ reversed = { value: node[:value], next: reversed }.freeze
318
+ node = node[:next]
319
+ end
320
+ reversed
321
+ end
281
322
  end
282
323
  end
283
324
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.10.0"
4
+ VERSION = "0.11.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.10.0
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young
@@ -50,7 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
50
  - !ruby/object:Gem::Version
51
51
  version: '0'
52
52
  requirements: []
53
- rubygems_version: 4.0.0.dev
53
+ rubygems_version: 4.0.10
54
54
  specification_version: 4
55
55
  summary: Atomic primitives for Ruby
56
56
  test_files: []