atomic-ruby 0.8.1 → 0.10.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 +4 -4
- data/CHANGELOG.md +14 -1
- data/README.md +21 -32
- data/ext/atomic_ruby/atomic_ruby.c +52 -15
- data/ext/atomic_ruby/atomic_ruby.h +2 -2
- data/ext/atomic_ruby/extconf.rb +0 -3
- data/lib/atomic-ruby/atom.rb +91 -6
- data/lib/atomic-ruby/atomic_boolean.rb +117 -0
- data/lib/atomic-ruby/atomic_count_down_latch.rb +129 -0
- data/lib/atomic-ruby/atomic_thread_pool.rb +188 -12
- data/lib/atomic-ruby/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b52931f98d46947d0d2f4dccffbe51e771c619d61b54457a223162be41624353
|
|
4
|
+
data.tar.gz: 3c91a27d99c3eb4d2a04f668bc5ebbc19c1c60b6a9fc688812df96c20bed37a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3f6d04175439390f6b9f29d91bf3cfad5b95018f7363e3cb341c7512857008e285d3a462363108cb54e613ed898153e157d7c41b1e113d92c38053a52e5d5be
|
|
7
|
+
data.tar.gz: aadb6f2b22fa068d8599b1e0aea30bc6e52d4b97198c4e4078e465d172f168b8548c9ab2f4bd174298aa2f5d02cd421c868a896c4b0d53689706edeb2414ff35
|
data/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.10.0] - 2025-11-09
|
|
4
|
+
|
|
5
|
+
- Add `AtomicThreadPool#active_count`
|
|
6
|
+
- Make native extension methods private
|
|
7
|
+
- Add YARD documentation and inline RBS type signatures
|
|
8
|
+
- Replace ruby 3.5 references with 4.0
|
|
9
|
+
|
|
10
|
+
## [0.9.0] - 2025-11-05
|
|
11
|
+
|
|
12
|
+
- Switch `AtomicThreadPool` back to atomics now that Ractor safety is lazy
|
|
13
|
+
- Don't enforce Ractor safety unless crossing Ractor boundaries
|
|
14
|
+
- Add `AtomicThreadPool#size` and `AtomicThreadPool#queue_size` aliases
|
|
15
|
+
|
|
3
16
|
## [0.8.1] - 2025-11-01
|
|
4
17
|
|
|
5
18
|
- Don't require `AtomicThreadPool#<<` to be given a shareable proc
|
|
6
19
|
|
|
7
20
|
## [0.8.0] - 2025-11-01
|
|
8
21
|
|
|
9
|
-
- Fix Ractor safety
|
|
22
|
+
- Fix Ractor safety
|
|
10
23
|
- Make `ArgumentError` messages consistent
|
|
11
24
|
- Implement write barriers for `Atom`
|
|
12
25
|
|
data/README.md
CHANGED
|
@@ -98,18 +98,7 @@ p latch.count #=> 0
|
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
> [!NOTE]
|
|
101
|
-
> `Atom`, `AtomicBoolean`, and `AtomicCountDownLatch` are Ractor-safe in Ruby
|
|
102
|
-
>
|
|
103
|
-
> When storing procs in atoms, create them using `Ractor.shareable_proc`, as `Ractor.make_shareable`
|
|
104
|
-
> cannot convert regular procs to shareable ones when the proc's context is not shareable.
|
|
105
|
-
>
|
|
106
|
-
> ```ruby
|
|
107
|
-
> # This will raise an error in Ruby 3.5+ (proc created in non-shareable context)
|
|
108
|
-
> atom = Atom.new(proc { puts "hello" })
|
|
109
|
-
>
|
|
110
|
-
> # Use this instead
|
|
111
|
-
> atom = Atom.new(Ractor.shareable_proc { puts "hello" })
|
|
112
|
-
> ```
|
|
101
|
+
> `Atom`, `AtomicBoolean`, and `AtomicCountDownLatch` are Ractor-safe in Ruby 4.0+.
|
|
113
102
|
|
|
114
103
|
## Benchmarks
|
|
115
104
|
|
|
@@ -221,9 +210,9 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
|
|
|
221
210
|
```
|
|
222
211
|
> bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
|
|
223
212
|
|
|
224
|
-
ruby version: ruby
|
|
213
|
+
ruby version: ruby 4.0.0dev (2025-11-08T15:08:09Z master 75d25a42e6) +YJIT +PRISM [arm64-darwin25]
|
|
225
214
|
concurrent-ruby version: 1.3.5
|
|
226
|
-
atomic-ruby version: 0.
|
|
215
|
+
atomic-ruby version: 0.10.0
|
|
227
216
|
|
|
228
217
|
Balances:
|
|
229
218
|
Synchronized Bank Account Balance: 975
|
|
@@ -231,9 +220,9 @@ Concurrent Ruby Atomic Bank Account Balance: 975
|
|
|
231
220
|
Atomic Ruby Atomic Bank Account Balance: 975
|
|
232
221
|
|
|
233
222
|
Benchmark Results:
|
|
234
|
-
Synchronized Bank Account: 5.
|
|
235
|
-
Concurrent Ruby Atomic Bank Account: 5.
|
|
236
|
-
Atomic Ruby Atomic Bank Account: 5.
|
|
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
|
|
237
226
|
```
|
|
238
227
|
|
|
239
228
|
</details>
|
|
@@ -312,29 +301,29 @@ end
|
|
|
312
301
|
```
|
|
313
302
|
> bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
|
|
314
303
|
|
|
315
|
-
ruby version: ruby
|
|
304
|
+
ruby version: ruby 4.0.0dev (2025-11-08T15:08:09Z master 75d25a42e6) +YJIT +PRISM [arm64-darwin25]
|
|
316
305
|
concurrent-ruby version: 1.3.5
|
|
317
|
-
atomic-ruby version: 0.
|
|
306
|
+
atomic-ruby version: 0.10.0
|
|
318
307
|
|
|
319
308
|
Warming up --------------------------------------
|
|
320
309
|
Synchronized Boolean Toggle
|
|
321
|
-
|
|
310
|
+
120.000 i/100ms
|
|
322
311
|
Concurrent Ruby Atomic Boolean Toggle
|
|
323
|
-
|
|
312
|
+
94.000 i/100ms
|
|
324
313
|
Atomic Ruby Atomic Boolean Toggle
|
|
325
|
-
|
|
314
|
+
100.000 i/100ms
|
|
326
315
|
Calculating -------------------------------------
|
|
327
316
|
Synchronized Boolean Toggle
|
|
328
|
-
1.
|
|
317
|
+
1.188k (± 8.9%) i/s (841.70 μs/i) - 5.880k in 5.002927s
|
|
329
318
|
Concurrent Ruby Atomic Boolean Toggle
|
|
330
|
-
|
|
319
|
+
889.224 (±11.8%) i/s (1.12 ms/i) - 4.418k in 5.073535s
|
|
331
320
|
Atomic Ruby Atomic Boolean Toggle
|
|
332
|
-
|
|
321
|
+
999.426 (± 4.3%) i/s (1.00 ms/i) - 5.000k in 5.012997s
|
|
333
322
|
|
|
334
323
|
Comparison:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
Concurrent Ruby Atomic Boolean Toggle:
|
|
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
|
|
338
327
|
```
|
|
339
328
|
|
|
340
329
|
</details>
|
|
@@ -390,13 +379,13 @@ puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
|
|
|
390
379
|
```
|
|
391
380
|
> bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
|
|
392
381
|
|
|
393
|
-
ruby version: ruby
|
|
382
|
+
ruby version: ruby 4.0.0dev (2025-11-08T15:08:09Z master 75d25a42e6) +YJIT +PRISM [arm64-darwin25]
|
|
394
383
|
concurrent-ruby version: 1.3.5
|
|
395
|
-
atomic-ruby version: 0.
|
|
384
|
+
atomic-ruby version: 0.10.0
|
|
396
385
|
|
|
397
386
|
Benchmark Results:
|
|
398
|
-
Concurrent Ruby Thread Pool: 5.
|
|
399
|
-
Atomic Ruby Atomic Thread Pool:
|
|
387
|
+
Concurrent Ruby Thread Pool: 5.56943 seconds
|
|
388
|
+
Atomic Ruby Atomic Thread Pool: 5.252876 seconds
|
|
400
389
|
```
|
|
401
390
|
|
|
402
391
|
</details>
|
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
typedef struct {
|
|
4
4
|
volatile VALUE value;
|
|
5
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
6
|
+
VALUE initialized_ractor;
|
|
7
|
+
#endif
|
|
5
8
|
} atomic_ruby_atom_t;
|
|
6
9
|
|
|
7
10
|
static void atomic_ruby_atom_mark(void *ptr) {
|
|
8
11
|
atomic_ruby_atom_t *atomic_ruby_atom = (atomic_ruby_atom_t *)ptr;
|
|
9
12
|
rb_gc_mark_movable(atomic_ruby_atom->value);
|
|
13
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
14
|
+
rb_gc_mark_movable(atomic_ruby_atom->initialized_ractor);
|
|
15
|
+
#endif
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
static void atomic_ruby_atom_free(void *ptr) {
|
|
@@ -21,6 +27,9 @@ static size_t atomic_ruby_atom_memsize(const void *ptr) {
|
|
|
21
27
|
static void atomic_ruby_atom_compact(void *ptr) {
|
|
22
28
|
atomic_ruby_atom_t *atomic_ruby_atom = (atomic_ruby_atom_t *)ptr;
|
|
23
29
|
atomic_ruby_atom->value = rb_gc_location(atomic_ruby_atom->value);
|
|
30
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
31
|
+
atomic_ruby_atom->initialized_ractor = rb_gc_location(atomic_ruby_atom->initialized_ractor);
|
|
32
|
+
#endif
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
static const rb_data_type_t atomic_ruby_atom_type = {
|
|
@@ -38,35 +47,54 @@ static const rb_data_type_t atomic_ruby_atom_type = {
|
|
|
38
47
|
#endif
|
|
39
48
|
};
|
|
40
49
|
|
|
50
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
51
|
+
static void ensure_value_shareable(VALUE self, atomic_ruby_atom_t *atom, VALUE value) {
|
|
52
|
+
bool check_shareable = NIL_P(atom->initialized_ractor);
|
|
53
|
+
|
|
54
|
+
if (!check_shareable) {
|
|
55
|
+
VALUE current_ractor = rb_funcall(rb_cRactor, rb_intern("current"), 0);
|
|
56
|
+
if (current_ractor != atom->initialized_ractor) {
|
|
57
|
+
check_shareable = true;
|
|
58
|
+
RB_OBJ_WRITE(self, &atom->initialized_ractor, Qnil);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (check_shareable && !rb_ractor_shareable_p(value)) {
|
|
63
|
+
rb_raise(rb_eArgError, "value must be a shareable object when used across ractors");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
#endif
|
|
67
|
+
|
|
41
68
|
static VALUE rb_cAtom_allocate(VALUE klass) {
|
|
42
69
|
atomic_ruby_atom_t *atomic_ruby_atom;
|
|
43
70
|
VALUE obj = TypedData_Make_Struct(klass, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
|
|
44
71
|
RB_OBJ_WRITE(obj, &atomic_ruby_atom->value, Qnil);
|
|
45
|
-
return obj;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
72
|
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
rb_raise(rb_eArgError, "value must be a shareable object");
|
|
52
|
-
}
|
|
53
|
-
}
|
|
73
|
+
VALUE current_ractor = rb_funcall(rb_cRactor, rb_intern("current"), 0);
|
|
74
|
+
RB_OBJ_WRITE(obj, &atomic_ruby_atom->initialized_ractor, current_ractor);
|
|
54
75
|
#endif
|
|
76
|
+
return obj;
|
|
77
|
+
}
|
|
55
78
|
|
|
56
79
|
static VALUE rb_cAtom_initialize(VALUE self, VALUE value) {
|
|
57
80
|
atomic_ruby_atom_t *atomic_ruby_atom;
|
|
58
81
|
TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
|
|
82
|
+
RB_OBJ_WRITE(self, &atomic_ruby_atom->value, value);
|
|
59
83
|
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
60
|
-
|
|
84
|
+
rb_obj_freeze(self);
|
|
85
|
+
FL_SET_RAW(self, RUBY_FL_SHAREABLE);
|
|
61
86
|
#endif
|
|
62
|
-
RB_OBJ_WRITE(self, &atomic_ruby_atom->value, value);
|
|
63
87
|
return self;
|
|
64
88
|
}
|
|
65
89
|
|
|
66
90
|
static VALUE rb_cAtom_value(VALUE self) {
|
|
67
91
|
atomic_ruby_atom_t *atomic_ruby_atom;
|
|
68
92
|
TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
|
|
69
|
-
|
|
93
|
+
VALUE value = (VALUE)RUBY_ATOMIC_PTR_LOAD(atomic_ruby_atom->value);
|
|
94
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
95
|
+
ensure_value_shareable(self, atomic_ruby_atom, value);
|
|
96
|
+
#endif
|
|
97
|
+
return value;
|
|
70
98
|
}
|
|
71
99
|
|
|
72
100
|
static VALUE rb_cAtom_swap(VALUE self) {
|
|
@@ -78,7 +106,7 @@ static VALUE rb_cAtom_swap(VALUE self) {
|
|
|
78
106
|
expected_old_value = atomic_ruby_atom->value;
|
|
79
107
|
new_value = rb_yield(expected_old_value);
|
|
80
108
|
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
81
|
-
|
|
109
|
+
ensure_value_shareable(self, atomic_ruby_atom, new_value);
|
|
82
110
|
#endif
|
|
83
111
|
} while (RUBY_ATOMIC_VALUE_CAS(atomic_ruby_atom->value, expected_old_value, new_value) != expected_old_value);
|
|
84
112
|
RB_OBJ_WRITTEN(self, expected_old_value, new_value);
|
|
@@ -86,6 +114,14 @@ static VALUE rb_cAtom_swap(VALUE self) {
|
|
|
86
114
|
return new_value;
|
|
87
115
|
}
|
|
88
116
|
|
|
117
|
+
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
118
|
+
static VALUE rb_cAtom_initialized_ractor(VALUE self) {
|
|
119
|
+
atomic_ruby_atom_t *atomic_ruby_atom;
|
|
120
|
+
TypedData_Get_Struct(self, atomic_ruby_atom_t, &atomic_ruby_atom_type, atomic_ruby_atom);
|
|
121
|
+
return atomic_ruby_atom->initialized_ractor;
|
|
122
|
+
}
|
|
123
|
+
#endif
|
|
124
|
+
|
|
89
125
|
RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
|
|
90
126
|
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
91
127
|
rb_ext_ractor_safe(true);
|
|
@@ -95,11 +131,12 @@ RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
|
|
|
95
131
|
VALUE rb_cAtom = rb_define_class_under(rb_mAtomicRuby, "Atom", rb_cObject);
|
|
96
132
|
|
|
97
133
|
rb_define_alloc_func(rb_cAtom, rb_cAtom_allocate);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
134
|
+
rb_define_private_method(rb_cAtom, "_initialize", rb_cAtom_initialize, 1);
|
|
135
|
+
rb_define_private_method(rb_cAtom, "_value", rb_cAtom_value, 0);
|
|
136
|
+
rb_define_private_method(rb_cAtom, "_swap", rb_cAtom_swap, 0);
|
|
101
137
|
|
|
102
138
|
#ifdef ATOMIC_RUBY_RACTOR_SAFE
|
|
139
|
+
rb_define_private_method(rb_cAtom, "_initialized_ractor", rb_cAtom_initialized_ractor, 0);
|
|
103
140
|
rb_define_const(rb_mAtomicRuby, "RACTOR_SAFE", Qtrue);
|
|
104
141
|
#else
|
|
105
142
|
rb_define_const(rb_mAtomicRuby, "RACTOR_SAFE", Qfalse);
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
#include "ruby.h"
|
|
5
5
|
#include "ruby/atomic.h"
|
|
6
|
-
#include "ruby/ractor.h"
|
|
7
6
|
#include "ruby/version.h"
|
|
8
7
|
|
|
9
|
-
#if RUBY_API_VERSION_CODE >=
|
|
8
|
+
#if RUBY_API_VERSION_CODE >= 40000
|
|
10
9
|
#define ATOMIC_RUBY_RACTOR_SAFE 1
|
|
10
|
+
#include "ruby/ractor.h"
|
|
11
11
|
#endif
|
|
12
12
|
|
|
13
13
|
#endif /* ATOMIC_RUBY_ATOM_H */
|
data/ext/atomic_ruby/extconf.rb
CHANGED
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "mkmf"
|
|
4
4
|
|
|
5
|
-
# Makes all symbols private by default to avoid unintended conflict
|
|
6
|
-
# with other gems. To explicitly export symbols you can use RUBY_FUNC_EXPORTED
|
|
7
|
-
# selectively, or entirely remove this flag.
|
|
8
5
|
append_cflags("-fvisibility=hidden")
|
|
9
6
|
|
|
10
7
|
create_makefile("atomic_ruby/atomic_ruby")
|
data/lib/atomic-ruby/atom.rb
CHANGED
|
@@ -1,29 +1,114 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require "atomic_ruby/atomic_ruby"
|
|
4
5
|
|
|
5
6
|
module AtomicRuby
|
|
7
|
+
# Provides atomic reference semantics using Compare-And-Swap (CAS) operations.
|
|
8
|
+
#
|
|
9
|
+
# An Atom allows for lock-free, thread-safe updates to a single reference value.
|
|
10
|
+
# The core operation is {#swap}, which atomically updates the value based on
|
|
11
|
+
# the current value, retrying if another thread modifies it concurrently.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# atom = Atom.new(0)
|
|
15
|
+
# atom.swap { |current_value| current_value + 1 }
|
|
16
|
+
# puts atom.value #=> 1
|
|
17
|
+
#
|
|
18
|
+
# @example Thread-safe counter
|
|
19
|
+
# counter = Atom.new(0)
|
|
20
|
+
# threads = 10.times.map do
|
|
21
|
+
# Thread.new { 100.times { counter.swap { |current_count| current_count + 1 } } }
|
|
22
|
+
# end
|
|
23
|
+
# threads.each(&:join)
|
|
24
|
+
# puts counter.value #=> 1000
|
|
25
|
+
#
|
|
26
|
+
# @example Non-mutating array operations
|
|
27
|
+
# atom = Atom.new([])
|
|
28
|
+
# atom.swap { |current_array| current_array + [1] }
|
|
29
|
+
# atom.swap { |current_array| current_array + [2] }
|
|
30
|
+
# puts atom.value #=> [1, 2]
|
|
31
|
+
#
|
|
32
|
+
# @note This class is Ractor-safe in Ruby 4.0+ when compiled with ractor support.
|
|
33
|
+
# Values that cross ractor boundaries are automatically made shareable.
|
|
6
34
|
class Atom
|
|
35
|
+
# Creates a new atomic reference with the given initial value.
|
|
36
|
+
#
|
|
37
|
+
# @param value [untyped] The initial value to store atomically
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# atom = Atom.new(42)
|
|
41
|
+
# atom = Atom.new([1, 2, 3])
|
|
42
|
+
# atom = Atom.new({ key: "value" })
|
|
43
|
+
#
|
|
44
|
+
# @rbs (untyped value) -> void
|
|
7
45
|
def initialize(value)
|
|
8
|
-
_initialize(
|
|
9
|
-
|
|
10
|
-
freeze if RACTOR_SAFE
|
|
46
|
+
_initialize(value)
|
|
11
47
|
end
|
|
12
48
|
|
|
49
|
+
# Returns the current value stored in the atom.
|
|
50
|
+
#
|
|
51
|
+
# This operation is atomic and thread-safe. The returned value reflects
|
|
52
|
+
# the state at the time of the call, but may change immediately after
|
|
53
|
+
# in concurrent environments.
|
|
54
|
+
#
|
|
55
|
+
# @return [untyped] The current atomic value
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# atom = Atom.new("hello")
|
|
59
|
+
# puts atom.value #=> "hello"
|
|
60
|
+
#
|
|
61
|
+
# @rbs () -> untyped
|
|
13
62
|
def value
|
|
14
63
|
_value
|
|
15
64
|
end
|
|
16
65
|
|
|
66
|
+
# Atomically updates the value using a compare-and-swap operation.
|
|
67
|
+
#
|
|
68
|
+
# The block receives the current value and must return the new value.
|
|
69
|
+
# If another thread modifies the atom between reading the current value
|
|
70
|
+
# and attempting to update it, the operation retries with the new current value.
|
|
71
|
+
#
|
|
72
|
+
# @yieldparam current_value [untyped] The current atomic value
|
|
73
|
+
# @yieldreturn [untyped] The new value to store atomically
|
|
74
|
+
# @return [untyped] The new value that was successfully stored
|
|
75
|
+
#
|
|
76
|
+
# @example Increment a counter
|
|
77
|
+
# atom = Atom.new(0)
|
|
78
|
+
# new_value = atom.swap { |current_value| current_value + 1 }
|
|
79
|
+
# puts new_value #=> 1
|
|
80
|
+
#
|
|
81
|
+
# @example Append to array (non-mutating)
|
|
82
|
+
# atom = Atom.new([1, 2])
|
|
83
|
+
# atom.swap { |current_array| current_array + [3] }
|
|
84
|
+
# puts atom.value #=> [1, 2, 3]
|
|
85
|
+
#
|
|
86
|
+
# @example Conditional update
|
|
87
|
+
# atom = Atom.new(10)
|
|
88
|
+
# atom.swap { |current_value| current_value > 5 ? current_value * 2 : current_value }
|
|
89
|
+
# puts atom.value #=> 20
|
|
90
|
+
#
|
|
91
|
+
# @rbs () { (untyped) -> untyped } -> untyped
|
|
17
92
|
def swap(&block)
|
|
18
93
|
_swap do |old_value|
|
|
19
|
-
|
|
94
|
+
make_shareable_if_needed(block.call(old_value))
|
|
20
95
|
end
|
|
21
96
|
end
|
|
22
97
|
|
|
23
98
|
private
|
|
24
99
|
|
|
25
|
-
|
|
26
|
-
|
|
100
|
+
# Makes a value shareable when crossing ractor boundaries.
|
|
101
|
+
#
|
|
102
|
+
# This method ensures ractor safety by automatically making values
|
|
103
|
+
# shareable when they need to cross ractor boundaries in Ruby 4.0+.
|
|
104
|
+
#
|
|
105
|
+
# @param value [untyped] The value to potentially make shareable
|
|
106
|
+
# @return [untyped] The original value or shareable version
|
|
107
|
+
#
|
|
108
|
+
# @rbs (untyped value) -> untyped
|
|
109
|
+
def make_shareable_if_needed(value)
|
|
110
|
+
if RACTOR_SAFE &&
|
|
111
|
+
(_initialized_ractor.nil? || Ractor.current != _initialized_ractor)
|
|
27
112
|
Ractor.make_shareable(value)
|
|
28
113
|
else
|
|
29
114
|
value
|
|
@@ -1,9 +1,50 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require_relative "atom"
|
|
4
5
|
|
|
5
6
|
module AtomicRuby
|
|
7
|
+
# Provides atomic boolean semantics with thread-safe toggle operations.
|
|
8
|
+
#
|
|
9
|
+
# AtomicBoolean wraps a boolean value in an atomic reference, providing
|
|
10
|
+
# lock-free operations for common boolean manipulations like toggling,
|
|
11
|
+
# setting to true/false, and checking the current state.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# boolean = AtomicBoolean.new(false)
|
|
15
|
+
# puts boolean.false? #=> true
|
|
16
|
+
# boolean.toggle
|
|
17
|
+
# puts boolean.true? #=> true
|
|
18
|
+
#
|
|
19
|
+
# @example Thread-safe toggle
|
|
20
|
+
# boolean = AtomicBoolean.new(false)
|
|
21
|
+
# threads = 10.times.map do
|
|
22
|
+
# Thread.new { 100.times { boolean.toggle } }
|
|
23
|
+
# end
|
|
24
|
+
# threads.each(&:join)
|
|
25
|
+
# # Final state depends on whether total toggles is even or odd
|
|
26
|
+
#
|
|
27
|
+
# @example Atomic flag setting
|
|
28
|
+
# flag = AtomicBoolean.new(false)
|
|
29
|
+
# flag.make_true
|
|
30
|
+
# puts flag.value #=> true
|
|
31
|
+
#
|
|
32
|
+
# @note This class is Ractor-safe in Ruby 4.0+ when compiled with ractor support.
|
|
6
33
|
class AtomicBoolean
|
|
34
|
+
# Creates a new atomic boolean with the given initial value.
|
|
35
|
+
#
|
|
36
|
+
# @param boolean [true, false] The initial boolean value
|
|
37
|
+
# @raise [ArgumentError] if the value is not a boolean (TrueClass or FalseClass)
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# boolean = AtomicBoolean.new(true)
|
|
41
|
+
# boolean = AtomicBoolean.new(false)
|
|
42
|
+
#
|
|
43
|
+
# @example Invalid usage
|
|
44
|
+
# AtomicBoolean.new(nil) #=> raises ArgumentError
|
|
45
|
+
# AtomicBoolean.new("true") #=> raises ArgumentError
|
|
46
|
+
#
|
|
47
|
+
# @rbs (bool boolean) -> void
|
|
7
48
|
def initialize(boolean)
|
|
8
49
|
unless boolean.is_a?(TrueClass) || boolean.is_a?(FalseClass)
|
|
9
50
|
raise ArgumentError, "boolean must be a TrueClass or FalseClass"
|
|
@@ -14,26 +55,102 @@ module AtomicRuby
|
|
|
14
55
|
Ractor.make_shareable(self) if RACTOR_SAFE
|
|
15
56
|
end
|
|
16
57
|
|
|
58
|
+
# Returns the current boolean value stored in the atom.
|
|
59
|
+
#
|
|
60
|
+
# This operation is atomic and thread-safe. The returned value reflects
|
|
61
|
+
# the state at the time of the call, but may change immediately after
|
|
62
|
+
# in concurrent environments.
|
|
63
|
+
#
|
|
64
|
+
# @return [true, false] The current atomic boolean value
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# boolean = AtomicBoolean.new(true)
|
|
68
|
+
# puts boolean.value #=> true
|
|
69
|
+
#
|
|
70
|
+
# @rbs () -> bool
|
|
17
71
|
def value
|
|
18
72
|
@boolean.value
|
|
19
73
|
end
|
|
20
74
|
|
|
75
|
+
# Tests if the current value is true.
|
|
76
|
+
#
|
|
77
|
+
# @return [true, false] true if the atomic value is true, false otherwise
|
|
78
|
+
#
|
|
79
|
+
# @example
|
|
80
|
+
# boolean = AtomicBoolean.new(true)
|
|
81
|
+
# puts boolean.true? #=> true
|
|
82
|
+
# puts boolean.false? #=> false
|
|
83
|
+
#
|
|
84
|
+
# @rbs () -> bool
|
|
21
85
|
def true?
|
|
22
86
|
value == true
|
|
23
87
|
end
|
|
24
88
|
|
|
89
|
+
# Tests if the current value is false.
|
|
90
|
+
#
|
|
91
|
+
# @return [true, false] true if the atomic value is false, false otherwise
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# boolean = AtomicBoolean.new(false)
|
|
95
|
+
# puts boolean.false? #=> true
|
|
96
|
+
# puts boolean.true? #=> false
|
|
97
|
+
#
|
|
98
|
+
# @rbs () -> bool
|
|
25
99
|
def false?
|
|
26
100
|
value == false
|
|
27
101
|
end
|
|
28
102
|
|
|
103
|
+
# Atomically sets the value to true.
|
|
104
|
+
#
|
|
105
|
+
# This operation uses compare-and-swap to ensure atomicity,
|
|
106
|
+
# making it safe for concurrent access.
|
|
107
|
+
#
|
|
108
|
+
# @return [true] Always returns true (the new value)
|
|
109
|
+
#
|
|
110
|
+
# @example
|
|
111
|
+
# boolean = AtomicBoolean.new(false)
|
|
112
|
+
# boolean.make_true
|
|
113
|
+
# puts boolean.value #=> true
|
|
114
|
+
#
|
|
115
|
+
# @rbs () -> true
|
|
29
116
|
def make_true
|
|
30
117
|
@boolean.swap { true }
|
|
31
118
|
end
|
|
32
119
|
|
|
120
|
+
# Atomically sets the value to false.
|
|
121
|
+
#
|
|
122
|
+
# This operation uses compare-and-swap to ensure atomicity,
|
|
123
|
+
# making it safe for concurrent access.
|
|
124
|
+
#
|
|
125
|
+
# @return [false] Always returns false (the new value)
|
|
126
|
+
#
|
|
127
|
+
# @example
|
|
128
|
+
# boolean = AtomicBoolean.new(true)
|
|
129
|
+
# boolean.make_false
|
|
130
|
+
# puts boolean.value #=> false
|
|
131
|
+
#
|
|
132
|
+
# @rbs () -> false
|
|
33
133
|
def make_false
|
|
34
134
|
@boolean.swap { false }
|
|
35
135
|
end
|
|
36
136
|
|
|
137
|
+
# Atomically toggles the boolean value.
|
|
138
|
+
#
|
|
139
|
+
# Changes true to false and false to true using a compare-and-swap
|
|
140
|
+
# operation, making it safe for concurrent access from multiple threads.
|
|
141
|
+
#
|
|
142
|
+
# @return [true, false] The new boolean value after toggling
|
|
143
|
+
#
|
|
144
|
+
# @example
|
|
145
|
+
# boolean = AtomicBoolean.new(false)
|
|
146
|
+
# boolean.toggle #=> true
|
|
147
|
+
# boolean.toggle #=> false
|
|
148
|
+
#
|
|
149
|
+
# @example Thread-safe toggling
|
|
150
|
+
# boolean = AtomicBoolean.new(false)
|
|
151
|
+
# 10.times.map { Thread.new { boolean.toggle } }.each(&:join)
|
|
152
|
+
#
|
|
153
|
+
# @rbs () -> bool
|
|
37
154
|
def toggle
|
|
38
155
|
@boolean.swap { |current_value| !current_value }
|
|
39
156
|
end
|
|
@@ -1,12 +1,69 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require_relative "atom"
|
|
4
5
|
|
|
5
6
|
module AtomicRuby
|
|
7
|
+
# Provides a countdown synchronization primitive using atomic operations.
|
|
8
|
+
#
|
|
9
|
+
# AtomicCountDownLatch is a synchronization aid that allows one or more threads
|
|
10
|
+
# to wait until a set of operations being performed in other threads completes.
|
|
11
|
+
# The latch is initialized with a count, and threads can wait for the count
|
|
12
|
+
# to reach zero or decrement the count atomically.
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# latch = AtomicCountDownLatch.new(3)
|
|
16
|
+
#
|
|
17
|
+
# # Start 3 worker threads
|
|
18
|
+
# 3.times do |worker_id|
|
|
19
|
+
# Thread.new do
|
|
20
|
+
# puts "Worker #{worker_id} starting"
|
|
21
|
+
# sleep(rand(2))
|
|
22
|
+
# puts "Worker #{worker_id} finished"
|
|
23
|
+
# latch.count_down
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# latch.wait # Wait for all workers to complete
|
|
28
|
+
# puts "All workers finished!"
|
|
29
|
+
#
|
|
30
|
+
# @example Waiting for initialization
|
|
31
|
+
# latch = AtomicCountDownLatch.new(1)
|
|
32
|
+
#
|
|
33
|
+
# # Start initialization in background
|
|
34
|
+
# Thread.new do
|
|
35
|
+
# puts "Initializing..."
|
|
36
|
+
# sleep(2)
|
|
37
|
+
# puts "Initialization complete"
|
|
38
|
+
# latch.count_down
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# puts "Waiting for initialization..."
|
|
42
|
+
# latch.wait
|
|
43
|
+
# puts "Ready to proceed!"
|
|
44
|
+
#
|
|
45
|
+
# @note This class is Ractor-safe in Ruby 4.0+ when compiled with ractor support.
|
|
6
46
|
class AtomicCountDownLatch
|
|
7
47
|
class Error < StandardError; end
|
|
48
|
+
|
|
49
|
+
# Error raised when attempting to count down a latch that is already at zero.
|
|
8
50
|
class AlreadyCountedDownError < Error; end
|
|
9
51
|
|
|
52
|
+
# Creates a new countdown latch with the specified count.
|
|
53
|
+
#
|
|
54
|
+
# @param count [Integer] The initial count value (must be positive)
|
|
55
|
+
#
|
|
56
|
+
# @raise [ArgumentError] if count is not a positive integer
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# latch = AtomicCountDownLatch.new(5) # Wait for 5 events
|
|
60
|
+
# latch = AtomicCountDownLatch.new(1) # Simple binary latch
|
|
61
|
+
#
|
|
62
|
+
# @example Invalid usage
|
|
63
|
+
# AtomicCountDownLatch.new(0) #=> raises ArgumentError
|
|
64
|
+
# AtomicCountDownLatch.new(-1) #=> raises ArgumentError
|
|
65
|
+
#
|
|
66
|
+
# @rbs (Integer count) -> void
|
|
10
67
|
def initialize(count)
|
|
11
68
|
unless count.is_a?(Integer) && count > 0
|
|
12
69
|
raise ArgumentError, "count must be a positive Integer"
|
|
@@ -17,10 +74,48 @@ module AtomicRuby
|
|
|
17
74
|
Ractor.make_shareable(self) if RACTOR_SAFE
|
|
18
75
|
end
|
|
19
76
|
|
|
77
|
+
# Returns the current count value.
|
|
78
|
+
#
|
|
79
|
+
# This operation is atomic and thread-safe. The returned value reflects
|
|
80
|
+
# the state at the time of the call, but may change immediately after
|
|
81
|
+
# in concurrent environments.
|
|
82
|
+
#
|
|
83
|
+
# @return [Integer] The current count (0 or positive)
|
|
84
|
+
#
|
|
85
|
+
# @example
|
|
86
|
+
# latch = AtomicCountDownLatch.new(3)
|
|
87
|
+
# puts latch.count #=> 3
|
|
88
|
+
# latch.count_down
|
|
89
|
+
# puts latch.count #=> 2
|
|
90
|
+
#
|
|
91
|
+
# @rbs () -> Integer
|
|
20
92
|
def count
|
|
21
93
|
@count.value
|
|
22
94
|
end
|
|
23
95
|
|
|
96
|
+
# Atomically decrements the count by one.
|
|
97
|
+
#
|
|
98
|
+
# If the count reaches zero, any threads waiting on {#wait} will be unblocked.
|
|
99
|
+
# This operation uses compare-and-swap to ensure atomicity and prevent
|
|
100
|
+
# race conditions.
|
|
101
|
+
#
|
|
102
|
+
# @return [Integer] The new count value after decrementing
|
|
103
|
+
#
|
|
104
|
+
# @raise [AlreadyCountedDownError] if the count is already zero
|
|
105
|
+
#
|
|
106
|
+
# @example
|
|
107
|
+
# latch = AtomicCountDownLatch.new(2)
|
|
108
|
+
# latch.count_down #=> 1
|
|
109
|
+
# latch.count_down #=> 0
|
|
110
|
+
# latch.count_down #=> raises AlreadyCountedDownError
|
|
111
|
+
#
|
|
112
|
+
# @example Thread-safe countdown
|
|
113
|
+
# latch = AtomicCountDownLatch.new(10)
|
|
114
|
+
# 10.times do
|
|
115
|
+
# Thread.new { latch.count_down }
|
|
116
|
+
# end
|
|
117
|
+
#
|
|
118
|
+
# @rbs () -> Integer
|
|
24
119
|
def count_down
|
|
25
120
|
already_counted_down = false
|
|
26
121
|
new_count = @count.swap do |current_count|
|
|
@@ -36,6 +131,40 @@ module AtomicRuby
|
|
|
36
131
|
new_count
|
|
37
132
|
end
|
|
38
133
|
|
|
134
|
+
# Blocks the current thread until the count reaches zero.
|
|
135
|
+
#
|
|
136
|
+
# This method will block the calling thread until other threads have
|
|
137
|
+
# called {#count_down} enough times to reduce the count to zero.
|
|
138
|
+
# The method uses busy-waiting (Thread.pass) to check the count.
|
|
139
|
+
#
|
|
140
|
+
# @return [void]
|
|
141
|
+
#
|
|
142
|
+
# @example Simple wait
|
|
143
|
+
# latch = AtomicCountDownLatch.new(1)
|
|
144
|
+
#
|
|
145
|
+
# Thread.new do
|
|
146
|
+
# sleep(2)
|
|
147
|
+
# latch.count_down
|
|
148
|
+
# end
|
|
149
|
+
#
|
|
150
|
+
# latch.wait # Blocks for ~2 seconds
|
|
151
|
+
# puts "Latch opened!"
|
|
152
|
+
#
|
|
153
|
+
# @example Waiting for multiple events
|
|
154
|
+
# latch = AtomicCountDownLatch.new(3)
|
|
155
|
+
#
|
|
156
|
+
# 3.times do |event_id|
|
|
157
|
+
# Thread.new do
|
|
158
|
+
# sleep(1 + event_id)
|
|
159
|
+
# puts "Event #{event_id} completed"
|
|
160
|
+
# latch.count_down
|
|
161
|
+
# end
|
|
162
|
+
# end
|
|
163
|
+
#
|
|
164
|
+
# latch.wait # Waits for all 3 events
|
|
165
|
+
# puts "All events completed!"
|
|
166
|
+
#
|
|
167
|
+
# @rbs () -> void
|
|
39
168
|
def wait
|
|
40
169
|
Thread.pass while @count.value > 0
|
|
41
170
|
end
|
|
@@ -1,15 +1,68 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require_relative "atom"
|
|
4
5
|
|
|
5
6
|
module AtomicRuby
|
|
7
|
+
# Provides a fixed-size thread pool using atomic operations for work queuing.
|
|
8
|
+
#
|
|
9
|
+
# AtomicThreadPool maintains a fixed number of worker threads that process
|
|
10
|
+
# work items from an atomic queue. The pool uses compare-and-swap operations
|
|
11
|
+
# for thread-safe work enqueueing and state management.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# pool = AtomicThreadPool.new(size: 4)
|
|
15
|
+
# pool << proc { puts "Hello from worker thread!" }
|
|
16
|
+
# pool << proc { puts "Another work item" }
|
|
17
|
+
# pool.shutdown
|
|
18
|
+
#
|
|
19
|
+
# @example Processing work with results
|
|
20
|
+
# results = []
|
|
21
|
+
# pool = AtomicThreadPool.new(size: 2, name: "Calculator")
|
|
22
|
+
#
|
|
23
|
+
# 10.times do |index|
|
|
24
|
+
# pool << proc { results << index * 2 }
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# pool.shutdown
|
|
28
|
+
# puts results.sort #=> [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
|
|
29
|
+
#
|
|
30
|
+
# @example Monitoring pool state
|
|
31
|
+
# pool = AtomicThreadPool.new(size: 3)
|
|
32
|
+
# puts pool.length #=> 3
|
|
33
|
+
# puts pool.queue_length #=> 0
|
|
34
|
+
# puts pool.active_count #=> 0
|
|
35
|
+
#
|
|
36
|
+
# 5.times { pool << proc { sleep(1) } }
|
|
37
|
+
# puts pool.queue_length #=> 2 (3 workers busy, 2 queued)
|
|
38
|
+
# puts pool.active_count #=> 3 (3 workers processing)
|
|
39
|
+
#
|
|
40
|
+
# @note This class is NOT Ractor-safe as it contains mutable thread state
|
|
41
|
+
# that cannot be safely shared across ractors.
|
|
6
42
|
class AtomicThreadPool
|
|
7
43
|
class Error < StandardError; end
|
|
8
44
|
|
|
45
|
+
# Error raised when attempting to enqueue work after shutdown.
|
|
9
46
|
class EnqueuedWorkAfterShutdownError < Error
|
|
47
|
+
# @rbs () -> String
|
|
10
48
|
def message = "cannot queue work after shutdown"
|
|
11
49
|
end
|
|
12
50
|
|
|
51
|
+
# Creates a new thread pool with the specified size.
|
|
52
|
+
#
|
|
53
|
+
# @param size [Integer] The number of worker threads to create (must be positive)
|
|
54
|
+
# @param name [String, nil] Optional name for the thread pool (used in thread names)
|
|
55
|
+
#
|
|
56
|
+
# @raise [ArgumentError] if size is not a positive integer
|
|
57
|
+
# @raise [ArgumentError] if name is provided but not a string
|
|
58
|
+
#
|
|
59
|
+
# @example Create a basic pool
|
|
60
|
+
# pool = AtomicThreadPool.new(size: 4)
|
|
61
|
+
#
|
|
62
|
+
# @example Create a named pool
|
|
63
|
+
# pool = AtomicThreadPool.new(size: 2, name: "Database Workers")
|
|
64
|
+
#
|
|
65
|
+
# @rbs (size: Integer, ?name: String?) -> void
|
|
13
66
|
def initialize(size:, name: nil)
|
|
14
67
|
raise ArgumentError, "size must be a positive Integer" unless size.is_a?(Integer) && size > 0
|
|
15
68
|
raise ArgumentError, "name must be a String" unless name.nil? || name.is_a?(String)
|
|
@@ -17,30 +70,139 @@ module AtomicRuby
|
|
|
17
70
|
@size = size
|
|
18
71
|
@name = name
|
|
19
72
|
|
|
20
|
-
@
|
|
21
|
-
@
|
|
22
|
-
@
|
|
73
|
+
@state = Atom.new(queue: [], shutdown: false)
|
|
74
|
+
@started_thread_count = Atom.new(0)
|
|
75
|
+
@active_thread_count = Atom.new(0)
|
|
23
76
|
@threads = []
|
|
24
77
|
|
|
25
78
|
start
|
|
26
79
|
end
|
|
27
80
|
|
|
81
|
+
# Enqueues work to be executed by the thread pool.
|
|
82
|
+
#
|
|
83
|
+
# The work item must respond to #call (typically a Proc or lambda).
|
|
84
|
+
# Work items are executed in FIFO order by available worker threads.
|
|
85
|
+
# If all workers are busy, the work is queued atomically.
|
|
86
|
+
#
|
|
87
|
+
# @param work [#call] A callable object to be executed by a worker thread
|
|
88
|
+
#
|
|
89
|
+
# @raise [EnqueuedWorkAfterShutdownError] if the pool has been shut down
|
|
90
|
+
#
|
|
91
|
+
# @example Enqueue a simple task
|
|
92
|
+
# pool << proc { puts "Hello World" }
|
|
93
|
+
#
|
|
94
|
+
# @example Enqueue a lambda with parameters
|
|
95
|
+
# calculator = ->(a, b) { puts a + b }
|
|
96
|
+
# pool << proc { calculator.call(2, 3) }
|
|
97
|
+
#
|
|
98
|
+
# @example Enqueue work that captures variables
|
|
99
|
+
# name = "Alice"
|
|
100
|
+
# pool << proc { puts "Processing #{name}" }
|
|
101
|
+
#
|
|
102
|
+
# @rbs (Proc work) -> void
|
|
28
103
|
def <<(work)
|
|
29
104
|
state = @state.swap do |current_state|
|
|
30
|
-
|
|
31
|
-
|
|
105
|
+
if current_state[:shutdown]
|
|
106
|
+
current_state
|
|
107
|
+
else
|
|
108
|
+
current_state.merge(queue: [*current_state[:queue], work])
|
|
109
|
+
end
|
|
32
110
|
end
|
|
33
111
|
raise EnqueuedWorkAfterShutdownError if state[:shutdown]
|
|
34
112
|
end
|
|
35
113
|
|
|
114
|
+
# Returns the number of currently alive worker threads.
|
|
115
|
+
#
|
|
116
|
+
# This count decreases as the pool shuts down and threads terminate.
|
|
117
|
+
# During normal operation, this should equal the size parameter
|
|
118
|
+
# passed to the constructor.
|
|
119
|
+
#
|
|
120
|
+
# @return [Integer] The number of alive worker threads
|
|
121
|
+
#
|
|
122
|
+
# @example
|
|
123
|
+
# pool = AtomicThreadPool.new(size: 4)
|
|
124
|
+
# puts pool.length #=> 4
|
|
125
|
+
# pool.shutdown
|
|
126
|
+
# puts pool.length #=> 0
|
|
127
|
+
#
|
|
128
|
+
# @rbs () -> Integer
|
|
36
129
|
def length
|
|
37
130
|
@threads.select(&:alive?).length
|
|
38
131
|
end
|
|
132
|
+
# Alias for {#length}.
|
|
133
|
+
# @rbs () -> Integer
|
|
134
|
+
alias size length
|
|
39
135
|
|
|
136
|
+
# Returns the number of work items currently queued for execution.
|
|
137
|
+
#
|
|
138
|
+
# This represents work that has been enqueued but not yet picked up
|
|
139
|
+
# by a worker thread. A high queue length indicates that work is
|
|
140
|
+
# being submitted faster than it can be processed.
|
|
141
|
+
#
|
|
142
|
+
# @return [Integer] The number of queued work items
|
|
143
|
+
#
|
|
144
|
+
# @example
|
|
145
|
+
# pool = AtomicThreadPool.new(size: 2)
|
|
146
|
+
# 5.times { pool << proc { sleep(1) } }
|
|
147
|
+
# puts pool.queue_length #=> 3 (2 workers busy, 3 queued)
|
|
148
|
+
#
|
|
149
|
+
# @rbs () -> Integer
|
|
40
150
|
def queue_length
|
|
41
|
-
@queue.
|
|
151
|
+
@state.value[:queue].length
|
|
152
|
+
end
|
|
153
|
+
# Alias for {#queue_length}.
|
|
154
|
+
# @rbs () -> Integer
|
|
155
|
+
alias queue_size queue_length
|
|
156
|
+
|
|
157
|
+
# Returns the number of worker threads currently executing work.
|
|
158
|
+
#
|
|
159
|
+
# This represents threads that have picked up a work item and are
|
|
160
|
+
# actively processing it. The count includes threads in the middle
|
|
161
|
+
# of executing work.call, but excludes threads that are idle or
|
|
162
|
+
# waiting for work.
|
|
163
|
+
#
|
|
164
|
+
# @return [Integer] The number of threads actively processing work
|
|
165
|
+
#
|
|
166
|
+
# @example Monitor active workers
|
|
167
|
+
# pool = AtomicThreadPool.new(size: 4)
|
|
168
|
+
# puts pool.active_count #=> 0
|
|
169
|
+
#
|
|
170
|
+
# 5.times { pool << proc { sleep(1) } }
|
|
171
|
+
# sleep(0.1) # Give threads time to pick up work
|
|
172
|
+
# puts pool.active_count #=> 4 (all workers busy)
|
|
173
|
+
# puts pool.queue_length #=> 1 (one item still queued)
|
|
174
|
+
#
|
|
175
|
+
# @example Calculate total load
|
|
176
|
+
# total_load = pool.active_count + pool.queue_length
|
|
177
|
+
# puts "Total pending work: #{total_load}"
|
|
178
|
+
#
|
|
179
|
+
# @rbs () -> Integer
|
|
180
|
+
def active_count
|
|
181
|
+
@active_thread_count.value
|
|
42
182
|
end
|
|
43
183
|
|
|
184
|
+
# Gracefully shuts down the thread pool.
|
|
185
|
+
#
|
|
186
|
+
# This method:
|
|
187
|
+
# 1. Marks the pool as shutdown (preventing new work from being enqueued)
|
|
188
|
+
# 2. Waits for all currently queued work to complete
|
|
189
|
+
# 3. Waits for all worker threads to terminate
|
|
190
|
+
#
|
|
191
|
+
# After shutdown, all worker threads will be terminated and the pool
|
|
192
|
+
# cannot be restarted. Attempting to enqueue work after shutdown
|
|
193
|
+
# will raise an exception.
|
|
194
|
+
#
|
|
195
|
+
# @return [void]
|
|
196
|
+
#
|
|
197
|
+
# @raise [EnqueuedWorkAfterShutdownError] if work is enqueued after shutdown
|
|
198
|
+
#
|
|
199
|
+
# @example
|
|
200
|
+
# pool = AtomicThreadPool.new(size: 4)
|
|
201
|
+
# 10.times { |index| pool << proc { puts index } }
|
|
202
|
+
# pool.shutdown # waits for all work to complete
|
|
203
|
+
# puts pool.length #=> 0
|
|
204
|
+
#
|
|
205
|
+
# @rbs () -> void
|
|
44
206
|
def shutdown
|
|
45
207
|
already_shutdown = false
|
|
46
208
|
@state.swap do |current_state|
|
|
@@ -53,13 +215,21 @@ module AtomicRuby
|
|
|
53
215
|
end
|
|
54
216
|
return if already_shutdown
|
|
55
217
|
|
|
56
|
-
Thread.pass until @queue.empty?
|
|
218
|
+
Thread.pass until @state.value[:queue].empty?
|
|
57
219
|
|
|
58
220
|
@threads.each(&:join)
|
|
59
221
|
end
|
|
60
222
|
|
|
61
223
|
private
|
|
62
224
|
|
|
225
|
+
# Starts the worker threads for the thread pool.
|
|
226
|
+
#
|
|
227
|
+
# This method is called automatically during initialization.
|
|
228
|
+
# It creates the specified number of worker threads and waits
|
|
229
|
+
# for all threads to be fully started before returning.
|
|
230
|
+
#
|
|
231
|
+
# @return [void]
|
|
232
|
+
# @rbs () -> void
|
|
63
233
|
def start
|
|
64
234
|
@size.times do |num|
|
|
65
235
|
@threads << Thread.new(num) do |idx|
|
|
@@ -67,30 +237,36 @@ module AtomicRuby
|
|
|
67
237
|
thread_name << " for #{@name}" if @name
|
|
68
238
|
Thread.current.name = thread_name
|
|
69
239
|
|
|
70
|
-
@
|
|
240
|
+
@started_thread_count.swap { |current_count| current_count + 1 }
|
|
71
241
|
|
|
72
242
|
loop do
|
|
73
243
|
work = nil
|
|
74
244
|
should_shutdown = false
|
|
75
245
|
|
|
76
246
|
@state.swap do |current_state|
|
|
77
|
-
if current_state[:shutdown] &&
|
|
247
|
+
if current_state[:shutdown] && current_state[:queue].empty?
|
|
78
248
|
should_shutdown = true
|
|
249
|
+
current_state
|
|
250
|
+
elsif current_state[:queue].empty?
|
|
251
|
+
current_state
|
|
79
252
|
else
|
|
80
|
-
work =
|
|
253
|
+
work = current_state[:queue].first
|
|
254
|
+
current_state.merge(queue: current_state[:queue].drop(1))
|
|
81
255
|
end
|
|
82
|
-
current_state
|
|
83
256
|
end
|
|
84
257
|
|
|
85
258
|
if should_shutdown
|
|
86
259
|
break
|
|
87
260
|
elsif work
|
|
261
|
+
@active_thread_count.swap { |current_count| current_count + 1 }
|
|
88
262
|
begin
|
|
89
263
|
work.call
|
|
90
264
|
rescue => err
|
|
91
265
|
puts "#{thread_name} rescued:"
|
|
92
266
|
puts "#{err.class}: #{err.message}"
|
|
93
267
|
puts err.backtrace.join("\n")
|
|
268
|
+
ensure
|
|
269
|
+
@active_thread_count.swap { |current_count| current_count - 1 }
|
|
94
270
|
end
|
|
95
271
|
else
|
|
96
272
|
Thread.pass
|
|
@@ -100,7 +276,7 @@ module AtomicRuby
|
|
|
100
276
|
end
|
|
101
277
|
@threads.freeze
|
|
102
278
|
|
|
103
|
-
Thread.pass until @
|
|
279
|
+
Thread.pass until @started_thread_count.value == @size
|
|
104
280
|
end
|
|
105
281
|
end
|
|
106
282
|
end
|
data/lib/atomic-ruby/version.rb
CHANGED