atomic-ruby 0.6.6 → 0.7.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: 71ab1390a71e724460d0331491b033a45d4dd1fb19308d62ffdcbf68bf68b149
4
- data.tar.gz: e19d576cd3ad1756ea5b429b6d57ab5e34402b3cf19674c597abc80a9f4b5f36
3
+ metadata.gz: ed78c53bc97b5e75c87c0d41a6ca9c5ba86688acb0eafff182cdba2b45c1134b
4
+ data.tar.gz: 143b41531bde38087ce81761977d7b283ed066880641370e25543424cf163a41
5
5
  SHA512:
6
- metadata.gz: b0db49db3fde19a7a6c7fe3adc58f999943b09e2074b88a79489ad6103712c2c4c2abf88a0d73ab1c7fe9e51dffeaa24c18c5d5519738d71d5fe8d7512855a5e
7
- data.tar.gz: f2d93730a210f1886d83c9971be5fa73acd8fa754c727008fd355899dc9cb2dfe90bb1fb7791663e2457f37b2adc74078a71f8f812c0f105132bcc7f68994468
6
+ metadata.gz: 6ff9bf98c45ba8f404fbab1908befbb2a703d3067cf0739c7265996e7c7840a7bc8c8ffe5a6cbfe8608960679f3e6ba716f773e790c333420fe35f5b251355ec
7
+ data.tar.gz: df712bfcc529dc67e3017204da3670a7502e438a4706befcfa7c595f37c6d8024ed33f35d061fa7beaa4cd2680fe263b319747177aa59a978888fbf2ee621cdb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.1] - 2025-10-26
4
+
5
+ - Fix O(n) performance issue in `AtomicThreadPool#<<` by using linked list
6
+
7
+ ## [0.7.0] - 2025-10-20
8
+
9
+ - Improve thread safety, performance, and error handling across atomic classes
10
+
3
11
  ## [0.6.6] - 2025-10-16
4
12
 
5
13
  - Fix individual file requires
data/README.md CHANGED
@@ -212,7 +212,7 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
212
212
 
213
213
  ruby version: ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin25]
214
214
  concurrent-ruby version: 1.3.5
215
- atomic-ruby version: 0.6.4
215
+ atomic-ruby version: 0.7.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.110797 seconds
224
- Concurrent Ruby Atomic Bank Account: 5.118082 seconds
225
- Atomic Ruby Atomic Bank Account: 5.087287 seconds
223
+ Synchronized Bank Account: 5.102692 seconds
224
+ Concurrent Ruby Atomic Bank Account: 5.100103 seconds
225
+ Atomic Ruby Atomic Bank Account: 5.096461 seconds
226
226
  ```
227
227
 
228
228
  </details>
@@ -303,27 +303,27 @@ end
303
303
 
304
304
  ruby version: ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin25]
305
305
  concurrent-ruby version: 1.3.5
306
- atomic-ruby version: 0.6.4
306
+ atomic-ruby version: 0.7.0
307
307
 
308
308
  Warming up --------------------------------------
309
309
  Synchronized Boolean Toggle
310
- 103.000 i/100ms
310
+ 93.000 i/100ms
311
311
  Concurrent Ruby Atomic Boolean Toggle
312
- 84.000 i/100ms
312
+ 79.000 i/100ms
313
313
  Atomic Ruby Atomic Boolean Toggle
314
- 115.000 i/100ms
314
+ 87.000 i/100ms
315
315
  Calculating -------------------------------------
316
316
  Synchronized Boolean Toggle
317
- 1.052k (± 3.9%) i/s (950.20 μs/i) - 5.356k in 5.097149s
317
+ 889.613 (± 3.0%) i/s (1.12 ms/i) - 4.464k in 5.022732s
318
318
  Concurrent Ruby Atomic Boolean Toggle
319
- 872.0733.2%) i/s (1.15 ms/i) - 4.368k in 5.013857s
319
+ 803.4182.5%) i/s (1.24 ms/i) - 4.029k in 5.017952s
320
320
  Atomic Ruby Atomic Boolean Toggle
321
- 1.214k (± 1.5%) i/s (823.94 μs/i) - 6.095k in 5.023014s
321
+ 1.037k3.1%) i/s (964.07 μs/i) - 5.220k in 5.037558s
322
322
 
323
323
  Comparison:
324
- Atomic Ruby Atomic Boolean Toggle: 1213.7 i/s
325
- Synchronized Boolean Toggle: 1052.4 i/s - 1.15x slower
326
- Concurrent Ruby Atomic Boolean Toggle: 872.1 i/s - 1.39x slower
324
+ Atomic Ruby Atomic Boolean Toggle: 1037.3 i/s
325
+ Synchronized Boolean Toggle: 889.6 i/s - 1.17x slower
326
+ Concurrent Ruby Atomic Boolean Toggle: 803.4 i/s - 1.29x slower
327
327
  ```
328
328
 
329
329
  </details>
@@ -381,11 +381,11 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
381
381
 
382
382
  ruby version: ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin25]
383
383
  concurrent-ruby version: 1.3.5
384
- atomic-ruby version: 0.6.4
384
+ atomic-ruby version: 0.7.0
385
385
 
386
386
  Benchmark Results:
387
- Concurrent Ruby Thread Pool: 4.768847 seconds
388
- Atomic Ruby Atomic Thread Pool: 4.401936 seconds
387
+ Concurrent Ruby Thread Pool: 4.802586 seconds
388
+ Atomic Ruby Atomic Thread Pool: 4.466127 seconds
389
389
  ```
390
390
 
391
391
  </details>
@@ -4,14 +4,12 @@ require_relative "atom"
4
4
 
5
5
  module AtomicRuby
6
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}"
7
+ def initialize(boolean)
8
+ unless boolean.is_a?(TrueClass) || boolean.is_a?(FalseClass)
9
+ raise ArgumentError, "boolean must be a TrueClass or FalseClass, got #{boolean.class}"
12
10
  end
13
11
 
14
- @boolean = Atom.new(value)
12
+ @boolean = Atom.new(boolean)
15
13
 
16
14
  Ractor.make_shareable(self)
17
15
  end
@@ -4,12 +4,12 @@ require_relative "atom"
4
4
 
5
5
  module AtomicRuby
6
6
  class AtomicCountDownLatch
7
- class InvalidCountError < StandardError; end
8
- class AlreadyCountedDownError < StandardError; end
7
+ class Error < StandardError; end
8
+ class AlreadyCountedDownError < Error; end
9
9
 
10
10
  def initialize(count)
11
- unless count.is_a?(Integer)
12
- raise InvalidCountError, "expected count to be an `Integer`, got #{count.class}"
11
+ unless count.is_a?(Integer) && count > 0
12
+ raise ArgumentError, "count must be a positive Integer, got #{count.class}"
13
13
  end
14
14
 
15
15
  @count = Atom.new(count)
@@ -22,11 +22,18 @@ module AtomicRuby
22
22
  end
23
23
 
24
24
  def count_down
25
- unless @count.value > 0
26
- raise AlreadyCountedDownError, "count has already reached zero"
25
+ already_counted_down = false
26
+ new_count = @count.swap do |current_count|
27
+ if current_count == 0
28
+ already_counted_down = true
29
+ current_count
30
+ else
31
+ current_count - 1
32
+ end
27
33
  end
28
-
29
- @count.swap { |current_value| current_value - 1 }
34
+ raise AlreadyCountedDownError, "already counted down to zero" if already_counted_down
35
+
36
+ new_count
30
37
  end
31
38
 
32
39
  def wait
@@ -1,35 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "atom"
4
- require_relative "atomic_boolean"
4
+ require_relative "linked_list"
5
5
 
6
6
  module AtomicRuby
7
7
  class AtomicThreadPool
8
- class UnsupportedWorkTypeError < StandardError; end
9
- class InvalidWorkQueueingError < StandardError; end
8
+ class Error < StandardError; end
9
+
10
+ class EnqueuedWorkAfterShutdownError < Error
11
+ def message = "cannot queue work after shutdown"
12
+ end
10
13
 
11
14
  def initialize(size:, name: nil)
15
+ raise ArgumentError, "size must be a positive Integer" unless size.is_a?(Integer) && size > 0
16
+ raise ArgumentError, "name must be a String" unless name.nil? || name.is_a?(String)
17
+
12
18
  @size = size
13
19
  @name = name
14
- @queue = Atom.new([])
15
- @threads = []
20
+
21
+ @state = Atom.new(queue: LinkedList.new, shutdown: false)
16
22
  @started_threads = Atom.new(0)
17
- @stopping = AtomicBoolean.new(false)
23
+ @threads = []
18
24
 
19
25
  start
20
26
  end
21
27
 
22
28
  def <<(work)
23
- unless work.is_a?(Proc) || work == :stop
24
- raise UnsupportedWorkTypeError, "expected work to be a `Proc`, got #{work.class}"
25
- end
26
-
27
- if @stopping.true?
28
- raise InvalidWorkQueueingError, "cannot queue work during or after pool shutdown"
29
+ state = @state.swap do |current_state|
30
+ if current_state[:shutdown]
31
+ current_state
32
+ else
33
+ current_state.merge(queue: current_state[:queue].prepend(work))
34
+ end
29
35
  end
30
-
31
- @queue.swap { |current_queue| current_queue += [work] }
32
- true
36
+ raise EnqueuedWorkAfterShutdownError if state[:shutdown]
33
37
  end
34
38
 
35
39
  def length
@@ -37,20 +41,31 @@ module AtomicRuby
37
41
  end
38
42
 
39
43
  def queue_length
40
- @queue.value.length
44
+ @state.value[:queue].length
41
45
  end
42
46
 
43
47
  def shutdown
44
- self << :stop
48
+ already_shutdown = false
49
+ @state.swap do |current_state|
50
+ if current_state[:shutdown]
51
+ already_shutdown = true
52
+ current_state
53
+ else
54
+ current_state.merge(shutdown: true)
55
+ end
56
+ end
57
+ return if already_shutdown
58
+
59
+ Thread.pass until @state.value[:queue].empty?
60
+
45
61
  @threads.each(&:join)
46
- true
47
62
  end
48
63
 
49
64
  private
50
65
 
51
66
  def start
52
- @threads = @size.times.map do |num|
53
- Thread.new(num) do |idx|
67
+ @size.times do |num|
68
+ @threads << Thread.new(num) do |idx|
54
69
  thread_name = String.new("AtomicThreadPool thread #{idx}")
55
70
  thread_name << " for #{@name}" if @name
56
71
  Thread.current.name = thread_name
@@ -59,9 +74,23 @@ module AtomicRuby
59
74
 
60
75
  loop do
61
76
  work = nil
62
- @queue.swap { |current_queue| work = current_queue.last; current_queue[0..-2] }
63
- case work
64
- when Proc
77
+ should_shutdown = false
78
+
79
+ @state.swap do |current_state|
80
+ if current_state[:shutdown] && current_state[:queue].empty?
81
+ should_shutdown = true
82
+ current_state
83
+ elsif current_state[:queue].empty?
84
+ current_state
85
+ else
86
+ work = current_state[:queue].first
87
+ current_state.merge(queue: current_state[:queue].rest)
88
+ end
89
+ end
90
+
91
+ if should_shutdown
92
+ break
93
+ elsif work
65
94
  begin
66
95
  work.call
67
96
  rescue => err
@@ -69,18 +98,13 @@ module AtomicRuby
69
98
  puts "#{err.class}: #{err.message}"
70
99
  puts err.backtrace.join("\n")
71
100
  end
72
- when :stop
73
- @stopping.make_true
74
- when NilClass
75
- if @stopping.true?
76
- break
77
- else
78
- Thread.pass
79
- end
101
+ else
102
+ Thread.pass
80
103
  end
81
104
  end
82
105
  end
83
106
  end
107
+ @threads.freeze
84
108
 
85
109
  Thread.pass until @started_threads.value == @size
86
110
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicRuby
4
+ class LinkedList
5
+ Node = Data.define(:value, :next_node)
6
+
7
+ def initialize(head = nil)
8
+ @head = head
9
+ end
10
+
11
+ def prepend(value)
12
+ self.class.new(Node.new(value, @head))
13
+ end
14
+
15
+ def first
16
+ @head&.value
17
+ end
18
+
19
+ def rest
20
+ self.class.new(@head&.next_node)
21
+ end
22
+
23
+ def empty?
24
+ @head.nil?
25
+ end
26
+
27
+ def length
28
+ count = 0
29
+ current = @head
30
+ while current
31
+ count += 1
32
+ current = current.next_node
33
+ end
34
+ count
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.6.6"
4
+ VERSION = "0.7.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.6.6
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young
@@ -29,6 +29,7 @@ files:
29
29
  - lib/atomic-ruby/atomic_boolean.rb
30
30
  - lib/atomic-ruby/atomic_count_down_latch.rb
31
31
  - lib/atomic-ruby/atomic_thread_pool.rb
32
+ - lib/atomic-ruby/linked_list.rb
32
33
  - lib/atomic-ruby/version.rb
33
34
  homepage: https://github.com/joshuay03/atomic-ruby
34
35
  licenses:
@@ -50,7 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
51
  - !ruby/object:Gem::Version
51
52
  version: '0'
52
53
  requirements: []
53
- rubygems_version: 3.6.9
54
+ rubygems_version: 3.7.2
54
55
  specification_version: 4
55
56
  summary: Atomic primitives for Ruby
56
57
  test_files: []