farce 0.3.0-arm-linux-gnu → 0.4.0-arm-linux-gnu
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 +44 -0
- data/README.md +117 -1489
- data/docs/compatibility.md +74 -0
- data/docs/concurrency.md +151 -0
- data/docs/data-structures.md +313 -0
- data/docs/installation.md +63 -0
- data/docs/integrations.md +124 -0
- data/docs/introduction.md +266 -0
- data/docs/scheduling.md +176 -0
- data/docs/sharing.md +211 -0
- data/docs/shims.md +144 -0
- data/docs/utilities.md +93 -0
- data/docs/variants-and-scopes.md +92 -0
- data/lib/farce/abstract/flag.rb +6 -0
- data/lib/farce/backports.rb +274 -0
- data/lib/farce/engine/jruby/fiber_scheduler.rb +3 -0
- data/lib/farce/engine/jruby/scheduler_synchronization.rb +87 -0
- data/lib/farce/engine/ruby/3.4/null_port.rb +21 -0
- data/lib/farce/engine/ruby/3.4/patch.rb +67 -0
- data/lib/farce/engine/ruby/3.4/port.rb +13 -12
- data/lib/farce/engine/ruby/3.4/ractor_methods.rb +0 -1
- data/lib/farce/engine/ruby/4.0/null_port.rb +36 -0
- data/lib/farce/engine/ruby/4.0/port.rb +1 -2
- data/lib/farce/engine/ruby/4.1/null_port.rb +5 -0
- data/lib/farce/engine/ruby.rb +7 -0
- data/lib/farce/engine/shared/null_port.rb +18 -0
- data/lib/farce/engine/shared/port.rb +41 -3
- data/lib/farce/engine/shared/queue.rb +4 -4
- data/lib/farce/engine/shared/ractor_methods.rb +2 -2
- data/lib/farce/engine/shared/ractor_selector.rb +73 -0
- data/lib/farce/extension.rb +150 -0
- data/lib/farce/integrations/ractor_tmvar.rb +4 -1
- data/lib/farce/internal/fake_ractor.rb +10 -2
- data/lib/farce/internal/scheduler_lifecycle.rb +7 -4
- data/lib/farce/null_port.rb +12 -0
- data/lib/farce/system.rb +10 -2
- data/lib/farce/version.rb +1 -1
- data/lib/farce.rb +1 -1
- metadata +22 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
# @title Integrations
|
|
3
|
+
-->
|
|
4
|
+
|
|
5
|
+
# Integrations
|
|
6
|
+
|
|
7
|
+
Farce ships with a couple of integrations that are automatically loaded if and only if both farce and the other gem have also been loaded (it does not automatically load these gems, even if they are part of the current bundle). This is load-order independent.
|
|
8
|
+
|
|
9
|
+
## Active Support
|
|
10
|
+
|
|
11
|
+
The Active Support integration adds the following methods:
|
|
12
|
+
|
|
13
|
+
* For all data structures: `as_json`, `blank?`, `deep_dup`, and `duplicable?`
|
|
14
|
+
* For maps: `assert_valid_keys`, `compact_blank`, `reverse_merge`, `stringify_keys`, `symbolize_keys`, `to_param`, `to_query`, `with_defaults`, and `with_indifferent_access`
|
|
15
|
+
* For vectors: `compact_blank`, `excluding`, `from`, `including`, `inquiry`, `in_groups`, `in_groups_of`, `in_order_of`, `maximum`, `minimum`, `pluck`, `pick`, `split`, `to`, `to_fs`, `to_param`, `to_sentence`, `to_query`, `to_xml`, `second`, `third`, `fourth`, `fifth`, `forty_two`, `third_to_last`, and `second_to_last`
|
|
16
|
+
|
|
17
|
+
And the following features:
|
|
18
|
+
* Converting `ActiveSupport::HashWithIndifferentAccess` to a map via [`Farce.enfarce`](utilities.md#top-level-methods) will set up the correct key normalization.
|
|
19
|
+
* [`Farce::Clock`](utilities.md#additional-classes) understands `ActiveSupport::Duration`.
|
|
20
|
+
|
|
21
|
+
Other methods are already being inherited by various objects via `Object`, `Enumerable` for vectors, sets, and maps, `Numeric` for counters, etc.
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "active_support/all"
|
|
25
|
+
require "farce"
|
|
26
|
+
|
|
27
|
+
map = Farce::Map.new.with_indifferent_access
|
|
28
|
+
map[:a] = 10
|
|
29
|
+
|
|
30
|
+
map.blank? # => false
|
|
31
|
+
map["a"] # => 10
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The integration isn't triggered by loading `active_support`, but instead looks for `active_support/core_ext`, so you should require that (or `active_support/all`, which in turn requires it).
|
|
35
|
+
|
|
36
|
+
## Dry Types
|
|
37
|
+
|
|
38
|
+
Adds support for Farce [data structures](data-structures.md#data-structures) to [Dry Types](https://hanakai.org/learn/dry/dry-types):
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require "dry-types"
|
|
42
|
+
require "farce"
|
|
43
|
+
|
|
44
|
+
module Types
|
|
45
|
+
include Dry.Types()
|
|
46
|
+
include Farce.DryTypes()
|
|
47
|
+
|
|
48
|
+
IntegerVector = Vector.of(Coercible::Integer)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
numbers = Types::IntegerVector[["1", 2]]
|
|
52
|
+
numbers.class # => Farce::Vector
|
|
53
|
+
numbers.to_a # => [1, 2]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Check out the [dedicated documentation](gems/dry-types.md) to learn more.
|
|
57
|
+
|
|
58
|
+
## JSON, YAML, etc
|
|
59
|
+
|
|
60
|
+
It ships integrations for serialization (and some deserialization) with the following gems:
|
|
61
|
+
|
|
62
|
+
* For BSON support: `bson`
|
|
63
|
+
* For CBOR support: `cbor`
|
|
64
|
+
* For JSON support: `json` (from the Ruby standard library), `oj`, and `yajl`
|
|
65
|
+
* For MessagePack: `msgpack`. See the [detailed documentation](gems/msgpack.md)
|
|
66
|
+
* For YAML: `psych` (from the Ruby standard library)
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
require "json"
|
|
72
|
+
require "farce"
|
|
73
|
+
|
|
74
|
+
map = Farce::Map.new
|
|
75
|
+
map[:x] = Farce::Vector[1, 2, 3]
|
|
76
|
+
map.to_json # => '{"x":[1,2,3]}'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Concurrent Ruby
|
|
80
|
+
|
|
81
|
+
The integration adds the following features if [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) has been loaded:
|
|
82
|
+
|
|
83
|
+
* [Transaction support for `Concurrent::TVar`](concurrency.md#tvars)
|
|
84
|
+
* Automatic conversion of `Concurrent::Map` instances to [maps](data-structures.md#maps) via [`Farce.enfarce`](utilities.md#top-level-methods).
|
|
85
|
+
|
|
86
|
+
## Ractor Sharing
|
|
87
|
+
|
|
88
|
+
The integration adds the following features if [ractor-sharing](https://github.com/ko1/ractor-sharing) has been loaded:
|
|
89
|
+
|
|
90
|
+
* [Transaction support for `Ractor::TVar`](concurrency.md#tvars)
|
|
91
|
+
* Support for traversing and deep freezing `Ractor::TVar`, `Ractor::LockVar`, `Ractor::LockHash`, and `Ractor::KeyLockHash`
|
|
92
|
+
* Support for converting `Ractor::LockHash` and `Ractor::KeyLockHash` to [maps](data-structures.md#maps) via [`Farce.enfarce`](utilities.md#top-level-methods).
|
|
93
|
+
|
|
94
|
+
## Additional Integrations
|
|
95
|
+
|
|
96
|
+
* [`sorted_set`](https://github.com/knu/sorted_set): Add support for walking and converting them.
|
|
97
|
+
* [`ractor-tmvar`](https://github.com/yoshitsugu/ractor-tmvar): Adds transaction support.
|
|
98
|
+
* [`weakref`](https://github.com/ruby/weakref): Add support for walking and converting them.
|
|
99
|
+
|
|
100
|
+
## Disable Automatic Loading
|
|
101
|
+
|
|
102
|
+
You can set the environment variable `FARCE_AUTOLOAD_INTEGRATIONS` to `false` or `0` to disable automatic integration loading.
|
|
103
|
+
|
|
104
|
+
Or you can use `Farce.configure` to disable it. But you may have to do so before loading `farce` unless you're absolutely certain the other gem has not yet been loaded:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# This could go in an initializer
|
|
108
|
+
require "farce/config"
|
|
109
|
+
|
|
110
|
+
Farce.configure do |config|
|
|
111
|
+
config.autoload_integrations = false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
require "farce"
|
|
115
|
+
|
|
116
|
+
# you now need to load any integrations you might want explicitly
|
|
117
|
+
# these are available via "farce/integrations/#{gem_name}"
|
|
118
|
+
require "farce/integrations/json"
|
|
119
|
+
require "farce/integrations/concurrent"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Read Next
|
|
123
|
+
|
|
124
|
+
[README](../README.md#features) · [Compatibility and Dependencies](compatibility.md) · [Utilities and Custom Classes](utilities.md)
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
# @title Introduction
|
|
3
|
+
-->
|
|
4
|
+
|
|
5
|
+
# Introduction
|
|
6
|
+
|
|
7
|
+
Ruby 3.0 introduced two powerful concurrency primitives: **Fiber schedulers** and **Ractors**, besides the already existing **Threads**:
|
|
8
|
+
|
|
9
|
+
* **Threads** are the classic concurrency primitive in Ruby, and mostly map to native operating system threads. This way any system calls, IO, or other blocking operations will block a thread, allowing another one to run. However, on the official Ruby implementation (often referred to as CRuby or MRI), Threads cannot run in parallel due to the Global VM Lock (GVL). This means that even if you have multiple threads, only one of them can execute Ruby code at a time.
|
|
10
|
+
* **Ractors** provide parallelism, even on CRuby. They make it very hard to introduce concurrency issues between them, but put severe limitations on state sharing and cross-ractor communication. Ractors are only supported by CRuby, but that is generally not an issue, as other implementations, such as JRuby and TruffleRuby, support true parallelism with Threads. As of Ruby 4.0, Ractors are still considered experimental.
|
|
11
|
+
* **Fiber schedulers** allow you to run multiple Fibers concurrently on a single Thread, without having to explicitly pass control between them. This is a very efficient way to run concurrent code, especially for IO-bound workloads. As of Ruby 4.0, Fiber schedulers with IO support are still considered experimental.
|
|
12
|
+
|
|
13
|
+
All three of these need some form of **concurrency coordination** for shared state. These are usually the most complicated for Threads, as state is shared between them freely, and an interrupt can happen at any time. Fibers on the other hand have explicit control transfer (only on blocking operations or when giving up control), so their behavior is much more predictable. Ractors disallow sharing mutable state between them (or have built-in locking for the few cases where they allow it).
|
|
14
|
+
|
|
15
|
+
## The Problem
|
|
16
|
+
|
|
17
|
+
> [!NOTE]
|
|
18
|
+
> Please keep in mind that many of these libraries may gain better Ractor support in the future, and that the table might already be outdated. Feel free to open an issue if you find any inaccuracies. This is also in no way meant as a criticism of any of the libraries mentioned. They all have their own scopes, goals and priorities, and largely rely on volunteer contributions.
|
|
19
|
+
|
|
20
|
+
Both the Ruby core library, and other libraries, like the very popular [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) gem, provide plenty of primitives to solve this. However, most of them are not compatible with Ractors. And those that are, are then in turn usually not compatible with Fiber schedulers. Due to this, most of the popular frameworks and libraries out there do not support Ractors at all:
|
|
21
|
+
|
|
22
|
+
<table>
|
|
23
|
+
<thead>
|
|
24
|
+
<tr>
|
|
25
|
+
<th>Library</th>
|
|
26
|
+
<th>Example classes</th>
|
|
27
|
+
<th>Multithreading</th>
|
|
28
|
+
<th>Non-blocking Fibers</th>
|
|
29
|
+
<th>Cross-Ractor Usage</th>
|
|
30
|
+
</tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody>
|
|
33
|
+
<tr>
|
|
34
|
+
<td rowspan="5"><a href="https://www.ruby-lang.org/">Ruby</a></td>
|
|
35
|
+
<td>Array, Hash, String, Set, …</td>
|
|
36
|
+
<td>⚠️ not thread-safe</td>
|
|
37
|
+
<td>✅ supported</td>
|
|
38
|
+
<td>⚠️ immutable/copy/move only</td>
|
|
39
|
+
</tr>
|
|
40
|
+
<tr>
|
|
41
|
+
<td>Thread, Fiber</td>
|
|
42
|
+
<td>✅ supported</td>
|
|
43
|
+
<td>✅ supported</td>
|
|
44
|
+
<td>❌ <b>not supported</b></td>
|
|
45
|
+
</tr>
|
|
46
|
+
<tr>
|
|
47
|
+
<td>Mutex, ConditionVariable, Queue</td>
|
|
48
|
+
<td>✅ supported</td>
|
|
49
|
+
<td>⚠️ CRuby only</td>
|
|
50
|
+
<td>❌ <b>not supported</b></td>
|
|
51
|
+
</tr>
|
|
52
|
+
<tr>
|
|
53
|
+
<td>Ractor, Ractor::Port</td>
|
|
54
|
+
<td>✅ supported</td>
|
|
55
|
+
<td>❌ <b>not supported</b></td>
|
|
56
|
+
<td>✅ supported</td>
|
|
57
|
+
</tr>
|
|
58
|
+
<tr>
|
|
59
|
+
<td>WeakRef, Ruby::Box</td>
|
|
60
|
+
<td>⚠️ Main Ractor only</td>
|
|
61
|
+
<td>⚠️ Main Ractor only</td>
|
|
62
|
+
<td>❌ <b>not supported</b></td>
|
|
63
|
+
</tr>
|
|
64
|
+
<tr>
|
|
65
|
+
<td><a href="https://github.com/ruby-concurrency/concurrent-ruby">concurrent-ruby</a></td>
|
|
66
|
+
<td>Future, Exchanger, Tuple, …</td>
|
|
67
|
+
<td>⚠️ Main Ractor only</td>
|
|
68
|
+
<td>⚠️ Main Ractor only</td>
|
|
69
|
+
<td>❌ <b>not supported</b></td>
|
|
70
|
+
</tr>
|
|
71
|
+
<tr>
|
|
72
|
+
<td rowspan="2"><a href="https://github.com/socketry/async">async</a></td>
|
|
73
|
+
<td>Promise, Condition, Queue, …</td>
|
|
74
|
+
<td>✅ supported</td>
|
|
75
|
+
<td>✅ supported</td>
|
|
76
|
+
<td>❌ <b>not supported</b></td>
|
|
77
|
+
</tr>
|
|
78
|
+
<tr>
|
|
79
|
+
<td>Scheduler</td>
|
|
80
|
+
<td>⚠️ Main Ractor only</td>
|
|
81
|
+
<td>⚠️ Main Ractor only</td>
|
|
82
|
+
<td>❌ <b>not supported</b></td>
|
|
83
|
+
</tr>
|
|
84
|
+
<tr>
|
|
85
|
+
<td rowspan="3"><a href="https://github.com/mperham/ratomic">ratomic</a></td>
|
|
86
|
+
<td>Pool</td>
|
|
87
|
+
<td>⚠️ CRuby only<b>¹</b></td>
|
|
88
|
+
<td>❌ <b>not supported</b></td>
|
|
89
|
+
<td>⚠️ CRuby only<b>¹</b></td>
|
|
90
|
+
</tr>
|
|
91
|
+
<tr>
|
|
92
|
+
<td>LocalPool</td>
|
|
93
|
+
<td>⚠️ CRuby only<b>¹</b></td>
|
|
94
|
+
<td>⚠️ CRuby only<b>¹</b></td>
|
|
95
|
+
<td>⚠️ CRuby only<b>¹</b></td>
|
|
96
|
+
</tr>
|
|
97
|
+
<tr>
|
|
98
|
+
<td>Queue, Map</td>
|
|
99
|
+
<td>⚠️ CRuby only<b>¹</b></td>
|
|
100
|
+
<td>❌ <b>not supported</b></td>
|
|
101
|
+
<td>💣 <b>breaks isolation¹²</b></td>
|
|
102
|
+
</tr>
|
|
103
|
+
<tr>
|
|
104
|
+
<td><a href="https://github.com/MadBomber/ractor_queue">ractor_queue</a></td>
|
|
105
|
+
<td>RactorQueue</td>
|
|
106
|
+
<td>⚠️ CRuby only<b>¹</b></td>
|
|
107
|
+
<td>⚠️ CRuby only<b>¹</b></td>
|
|
108
|
+
<td>💣 <b>breaks isolation¹²</b></td>
|
|
109
|
+
</tr>
|
|
110
|
+
<tr>
|
|
111
|
+
<td>
|
|
112
|
+
<a href="https://github.com/hamstergem/hamster">hamster</a> /
|
|
113
|
+
<a href="https://github.com/immutable-ruby/immutable-ruby">immutable</a>
|
|
114
|
+
</td>
|
|
115
|
+
<td>Hash, Vector, Set, List, …</td>
|
|
116
|
+
<td>⚠️ Main Ractor only</td>
|
|
117
|
+
<td>⚠️ Main Ractor only</td>
|
|
118
|
+
<td>❌ <b>not supported</b></td>
|
|
119
|
+
</tr>
|
|
120
|
+
<tr>
|
|
121
|
+
<td><a href="https://github.com/ko1/ractor-sharing">ractor-sharing</a></td>
|
|
122
|
+
<td>TVar, LockVar, LockHash, …</td>
|
|
123
|
+
<td>⚠️ CRuby only<b>¹</b></td>
|
|
124
|
+
<td>❌ <b>not supported</b></td>
|
|
125
|
+
<td>⚠️ CRuby only<b>¹</b></td>
|
|
126
|
+
</tr>
|
|
127
|
+
<tr>
|
|
128
|
+
<td rowspan="2"><a href="https://github.com/jhawthorn/ractor_safe">ractor_safe</a></td>
|
|
129
|
+
<td>HashMap, AtomicInteger</td>
|
|
130
|
+
<td>⚠️ CRuby only</td>
|
|
131
|
+
<td>⚠️ CRuby only</td>
|
|
132
|
+
<td>⚠️ CRuby only</td>
|
|
133
|
+
</tr>
|
|
134
|
+
<tr>
|
|
135
|
+
<td>Queue</td>
|
|
136
|
+
<td>⚠️ CRuby only</td>
|
|
137
|
+
<td>❌ <b>not supported</b></td>
|
|
138
|
+
<td>⚠️ CRuby only</td>
|
|
139
|
+
</tr>
|
|
140
|
+
</tbody>
|
|
141
|
+
</table>
|
|
142
|
+
|
|
143
|
+
Notes:
|
|
144
|
+
1. Incorrectly flags mutable objects as frozen.
|
|
145
|
+
2. Breaking Ractor isolation introduces concurrency issues not just in application code, but in Ruby itself, possibly leading to segmentation faults or undefined behavior. Note that `ractor_queue` has the ability to enforce shareability, but this feature is opt-in.
|
|
146
|
+
|
|
147
|
+
## The Solution
|
|
148
|
+
|
|
149
|
+
**Farce is an attempt to solve this.** Code relying on Farce will work well with any concurrency primitive. Use an **event loop with the [async](https://github.com/socketry/async)** gem? Farce will fit right in. Running a **pool of Ractors** with [Kino](https://github.com/yaroslav/kino)? Farce got you covered! You manually manage a single **background thread**? Farce solves that, too!
|
|
150
|
+
|
|
151
|
+
And it does so in a non-invasive way. You should be able to use Farce alongside any other gem!
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# Using a counter as an example, there are many more classes provided by Farce
|
|
155
|
+
counter = Farce::Counter.new
|
|
156
|
+
|
|
157
|
+
# Works in code without concurrency
|
|
158
|
+
counter.add(5)
|
|
159
|
+
counter.to_i # => 5
|
|
160
|
+
|
|
161
|
+
# Works with threads
|
|
162
|
+
wait_for = 5.times.map do
|
|
163
|
+
Thread.new { counter.increment }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Works with ractors
|
|
167
|
+
wait_for += 5.times.map do
|
|
168
|
+
Ractor.new(counter) { it.increment }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Works with the async gem
|
|
172
|
+
Async do
|
|
173
|
+
5.times do
|
|
174
|
+
Async { counter.increment }
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
wait_for.each(&:join)
|
|
179
|
+
counter.to_i # => 20
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Farce also aims to be fast and efficient, with performance ranging from minimal overhead to outperforming other options.
|
|
183
|
+
|
|
184
|
+
It does so by choosing the best implementation for the situation. The counter in the above example will use Java's `AtomicLong` on JRuby and TruffleRuby in GraalVM mode, an `AtomicReference` on TruffleRuby in native mode, and an atomic, native counter on CRuby, so it runs without having to use any locks on any of these platforms.
|
|
185
|
+
|
|
186
|
+
This is especially useful when coordinating work between Fiber schedulers and Ractors:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
queue = Farce::Queue.new
|
|
190
|
+
|
|
191
|
+
# Background ractor pushing work into the queue
|
|
192
|
+
# This would not work with Ruby's built-in Queue
|
|
193
|
+
Ractor.new(queue) do |queue|
|
|
194
|
+
loop { queue.push expensive_work }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# This would not work with Ratomic::Queue
|
|
198
|
+
Async do
|
|
199
|
+
# Task waiting for work from the queue
|
|
200
|
+
Async { loop { do_something queue.pop } }
|
|
201
|
+
|
|
202
|
+
# Doesn't get blocked by the other task waiting for the queue
|
|
203
|
+
Async { unrelated_work }
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Example
|
|
208
|
+
|
|
209
|
+
A map and a mutable string can be shared across Ractors. The transaction keeps their updates together. This example deliberately creates many Ractors. For application workloads, use [a pool](scheduling.md#ractor-pools) to reuse workers.
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
require "farce"
|
|
213
|
+
|
|
214
|
+
map = Farce::Map.new
|
|
215
|
+
output = Farce::Mutable.new "Hello from Farce! The map has 0 entries, with a total sum of 0."
|
|
216
|
+
|
|
217
|
+
# Mutate output in another Ractor - this would not work with a String.
|
|
218
|
+
# Using Farce::Ractor here instead of Ractor so it also works on JRuby and TruffleRuby.
|
|
219
|
+
ractor = Farce::Ractor.new(output) do |output|
|
|
220
|
+
sleep rand # I laugh in the face of race conditions. Ha, ha, ha, ha!
|
|
221
|
+
output.gsub!("Farce", "✨ Farce ✨")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# No idea if the above ractor is done modifying output, but we don't need to worry!
|
|
225
|
+
# We'll just pick a random key one hundred times and increase its counter by one.
|
|
226
|
+
# Oh, and update the output string.
|
|
227
|
+
100.times.map do
|
|
228
|
+
|
|
229
|
+
# And of course we need to do it all in parallel for maximum performance!
|
|
230
|
+
# Let's ignore the fact that this would be much faster if we didn't.
|
|
231
|
+
# Starting 100 ractors is the main performance issue here.
|
|
232
|
+
# But that wouldn't be an interesting example, right?
|
|
233
|
+
Farce::Ractor.new(output, map) do |output, map|
|
|
234
|
+
key = %i[foo bar baz].sample
|
|
235
|
+
|
|
236
|
+
# Wrap the modifications in a transaction, so map and output never disagree
|
|
237
|
+
Farce.transaction(output, map) do |_, output, map|
|
|
238
|
+
|
|
239
|
+
# Increase the value we have for the given key by one!
|
|
240
|
+
map[key] ||= 0
|
|
241
|
+
map[key] += 1
|
|
242
|
+
|
|
243
|
+
# Could also replace the lines above with:
|
|
244
|
+
#
|
|
245
|
+
# map.upsert(key, 1) { it + 1 }
|
|
246
|
+
#
|
|
247
|
+
# … which would be atomic.
|
|
248
|
+
# But we're in a transaction, so everything is atomic! ☢️
|
|
249
|
+
|
|
250
|
+
output.sub!(/\d+ entries/, "#{map.size} entries")
|
|
251
|
+
output.sub!(/sum of \d+/, "sum of #{map.values.sum}")
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
end.each(&:join)
|
|
256
|
+
|
|
257
|
+
# Let's be sure that first ractor has completed its modification
|
|
258
|
+
ractor.join
|
|
259
|
+
|
|
260
|
+
# Hello from ✨ Farce ✨! The map has 3 entries with a total sum of 100.
|
|
261
|
+
puts output
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Read Next
|
|
265
|
+
|
|
266
|
+
[README](../README.md#features) · [Data Structures](data-structures.md) · [Concurrency](concurrency.md)
|
data/docs/scheduling.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
# @title Scheduling Code
|
|
3
|
+
-->
|
|
4
|
+
|
|
5
|
+
# Scheduling Code
|
|
6
|
+
|
|
7
|
+
Use Farce to schedule background work, reuse workers for parallel computation, or dispatch calls to the main Ractor. Tasks retain results, while schedulers and pools control where work runs.
|
|
8
|
+
|
|
9
|
+
## `Farce.on_main`
|
|
10
|
+
|
|
11
|
+
Some code has to be executed on the main Ractor, especially when working with legacy code that doesn't support Ractors.
|
|
12
|
+
|
|
13
|
+
You can pass a block to `Farce.on_main` to run code on the main Ractor. The method call will block until the code has been executed.
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# most of concurrent-ruby is not usable outside of the main-ractor
|
|
17
|
+
$tvar = Concurrent::TVar.new(0)
|
|
18
|
+
|
|
19
|
+
# let's run computation outside of the main ractor
|
|
20
|
+
Ractor.new do
|
|
21
|
+
value = compute_expensive_value
|
|
22
|
+
|
|
23
|
+
# need to report back to the main ractor
|
|
24
|
+
Farce.on_main(value) { $tvar.value = it }
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Sometimes you don't need to wait for the code to be done running on the main Ractor. In such cases, you can use `schedule`, as `on_main` returns a [scheduler](#schedulers) instance when called without a block:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
Ractor.new do
|
|
32
|
+
Farce.on_main.schedule do
|
|
33
|
+
sleep 1
|
|
34
|
+
puts "Hello from the main Ractor"
|
|
35
|
+
end
|
|
36
|
+
puts "Hello from the nested Ractor"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sleep 1.1
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## `Farce.in_parallel`
|
|
43
|
+
|
|
44
|
+
As you can see in the [shared-state example](introduction.md#example), creating Ractors ad hoc because you want to run something in parallel is quite expensive.
|
|
45
|
+
|
|
46
|
+
Use `Farce.in_parallel` to schedule parallel work. On CRuby, it manages a shared [Ractor pool](#ractor-pools), starting workers when the current ones cannot keep up with the load, up to the number of CPU cores. It shuts unused workers down after some inactivity. On JRuby and TruffleRuby, it starts a new thread for each task.
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
input = "this is my input"
|
|
50
|
+
Farce.in_parallel(input) { expensive_computation(it) }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Like most of Farce, this is completely opt-in. If you never use this feature, no pool is being set up and no extra Ractors are created.
|
|
54
|
+
|
|
55
|
+
## `Farce.schedule`
|
|
56
|
+
|
|
57
|
+
`Farce.in_parallel` might not always be what you want. Maybe you are working on a library and don't want to dictate a concurrency model?
|
|
58
|
+
|
|
59
|
+
You can use `Farce.schedule`, which will automatically figure out how to run code off-band. You can use `mode: :local` to force execution inside the current Ractor (which it will prefer by default if there already is some scheduler running, but not enforce).
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
# just run this asynchronously, don't care how
|
|
63
|
+
Farce.schedule("hello") { |message| puts message.upcase }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
"Some scheduler?" you might say? Some scheduler! It will automatically detect if there is a local scheduler. This can either be a Fiber scheduler, like [async](https://socketry.github.io/async/) or [Carbon Fiber](https://yaroslav.io/opensource/carbon_fiber), or an internal scheduler created by Farce:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
Async do
|
|
70
|
+
# this is basically the same as calling Async { do_something }
|
|
71
|
+
Farce.schedule { do_something }
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Schedulers
|
|
76
|
+
|
|
77
|
+
You can create your own `Farce::Scheduler`, running on a dedicated Ractor:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
scheduler = Farce::Scheduler.create
|
|
81
|
+
scheduler.schedule("hello") { |message| puts message }
|
|
82
|
+
scheduler.close # we're done, shut it down
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Or you can set one up as a Fiber scheduler for the current Thread:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
scheduler = Farce::Scheduler.new
|
|
89
|
+
Fiber.set_scheduler(scheduler)
|
|
90
|
+
Fiber.schedule { puts "Hello from the scheduler!" }
|
|
91
|
+
scheduler.close
|
|
92
|
+
Fiber.set_scheduler(nil) # drain scheduled work and detach the scheduler
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Scheduler instances are Ractor-shareable.
|
|
96
|
+
|
|
97
|
+
## Ractor Pools
|
|
98
|
+
|
|
99
|
+
`Farce::Pool` implements the same scheduling interface, but manages a pool of Ractors. This is what [`in_parallel`](#farcein_parallel) uses under the hood.
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
# At least four Ractors, up to ten. Launch a new one if a task waits longer than 100 milliseconds.
|
|
103
|
+
pool = Farce::Pool.new(min_size: 4, max_size: 10, grow_after: 0.1)
|
|
104
|
+
pool.schedule { puts "Hello from the pool!" }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Tasks
|
|
108
|
+
|
|
109
|
+
If you want to retrieve the result of an asynchronous operation, or monitor if it is still running, you can use `Farce::Task`:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
task = Farce::Task.schedule(1, 2) do |a, b|
|
|
113
|
+
sleep 1 # simulate intense computation
|
|
114
|
+
a + b
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
task.alive? # => true
|
|
118
|
+
task.value # => 3 (after approximately 1 second)
|
|
119
|
+
task.join # would block, but doesn't, as the task has already completed
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Tasks can also be created without immediately scheduling them, allowing you to control when and how they are executed:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
task = Farce::Task.new { :success }
|
|
126
|
+
task.alive? # => true
|
|
127
|
+
task.value(0.1) # => nil (after 100 milliseconds)
|
|
128
|
+
|
|
129
|
+
Farce.schedule(&task)
|
|
130
|
+
task.value # => :success
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This allows you to use third-party executors:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
require "concurrent"
|
|
137
|
+
executor = Concurrent.global_fast_executor
|
|
138
|
+
task = Farce::Task.new { :success }
|
|
139
|
+
executor.post(&task)
|
|
140
|
+
|
|
141
|
+
task.value # => :success
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Third-Party Fiber Schedulers
|
|
145
|
+
|
|
146
|
+
Both `Farce::Scheduler` and `Farce::Pool` run a Fiber scheduler under the hood to execute tasks. This is a Farce-internal scheduler by default, but you can replace it with your own if you want:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
# Use the fiber scheduler from the carbon_fiber gem.
|
|
150
|
+
pool = Farce::Pool.new { CarbonFiber::Scheduler.new }
|
|
151
|
+
pool.schedule { puts "⚡️ Running Carbon Fiber on a pool of Ractors! ⚡️" }
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
You can also use a different fiber scheduler as the default (in which case it will be picked up by `in_parallel` and `on_main` as well) by setting the `FARCE_FIBER_SCHEDULER` environment variable or using the `fiber_scheduler` configuration setting:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# This needs to happen before the first scheduler call.
|
|
158
|
+
Farce.configure do |config|
|
|
159
|
+
config.fiber_scheduler = :carbon_fiber
|
|
160
|
+
|
|
161
|
+
# or, alternatively:
|
|
162
|
+
config.fiber_scheduler { CarbonFiber::Scheduler.new }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
Farce.in_parallel do
|
|
166
|
+
Fiber.scheduler.class # => CarbonFiber::Scheduler
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
If a string or symbol is provided (like `carbon_fiber`), it will first resolve this to a constant (i.e., `CarbonFiber`). If that constant is a class, it will use that for creating the fiber scheduler. If it is a module, it will look for a `Scheduler` constant inside it, which matches the established pattern implemented by most gems, including `farce`, `async`, `carbon_fiber`, `libev_scheduler` (use `libev` as value), `itsi_scheduler` (use `itsi` as value), but it also works for gems that define their scheduler class at top level, like `fiber_scheduler`.
|
|
171
|
+
|
|
172
|
+
Note that most of these are outdated and don't work with any recent Ruby version, with the notable exception of `async` and `carbon_fiber`.
|
|
173
|
+
|
|
174
|
+
## Read Next
|
|
175
|
+
|
|
176
|
+
[README](../README.md#features) · [Concurrency](concurrency.md) · [Shims, Polyfills, and Extensions](shims.md)
|