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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +24 -24
- data/lib/atomic-ruby/atomic_thread_pool.rb +57 -16
- data/lib/atomic-ruby/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a9ae1ba9062331be3f6a5d74f170dbf88eb2ce6b142f3ef5077194ddeb1295e
|
|
4
|
+
data.tar.gz: 652fbf6d04d6efa3d57fc37643f85241e685950320d343e2341c155b0539bf93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|

|
|
4
|
-

|
|
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.
|
|
214
|
-
concurrent-ruby version: 1.3.
|
|
215
|
-
atomic-ruby version: 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.
|
|
224
|
-
Concurrent Ruby Atomic Bank Account: 5.
|
|
225
|
-
Atomic Ruby Atomic Bank Account: 5.
|
|
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.
|
|
305
|
-
concurrent-ruby version: 1.3.
|
|
306
|
-
atomic-ruby version: 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
|
-
|
|
310
|
+
135.000 i/100ms
|
|
311
311
|
Concurrent Ruby Atomic Boolean Toggle
|
|
312
|
-
|
|
312
|
+
117.000 i/100ms
|
|
313
313
|
Atomic Ruby Atomic Boolean Toggle
|
|
314
|
-
|
|
314
|
+
101.000 i/100ms
|
|
315
315
|
Calculating -------------------------------------
|
|
316
316
|
Synchronized Boolean Toggle
|
|
317
|
-
1.
|
|
317
|
+
1.414k (± 1.4%) i/s (707.15 μs/i) - 7.155k in 5.060692s
|
|
318
318
|
Concurrent Ruby Atomic Boolean Toggle
|
|
319
|
-
|
|
319
|
+
1.149k (± 2.9%) i/s (870.53 μs/i) - 5.850k in 5.097019s
|
|
320
320
|
Atomic Ruby Atomic Boolean Toggle
|
|
321
|
-
|
|
321
|
+
1.046k (± 2.1%) i/s (955.57 μs/i) - 5.252k in 5.021118s
|
|
322
322
|
|
|
323
323
|
Comparison:
|
|
324
|
-
Synchronized Boolean Toggle:
|
|
325
|
-
|
|
326
|
-
|
|
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.
|
|
383
|
-
concurrent-ruby version: 1.3.
|
|
384
|
-
atomic-ruby version: 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.
|
|
388
|
-
Atomic Ruby Atomic Thread Pool: 5.
|
|
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
|
-
# @
|
|
66
|
-
|
|
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(
|
|
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
|
-
|
|
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[:
|
|
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[:
|
|
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[:
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
|
data/lib/atomic-ruby/version.rb
CHANGED
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.
|
|
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.
|
|
53
|
+
rubygems_version: 4.0.10
|
|
54
54
|
specification_version: 4
|
|
55
55
|
summary: Atomic primitives for Ruby
|
|
56
56
|
test_files: []
|