farce 0.3.0 → 0.4.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +117 -1489
  4. data/docs/compatibility.md +74 -0
  5. data/docs/concurrency.md +151 -0
  6. data/docs/data-structures.md +313 -0
  7. data/docs/installation.md +63 -0
  8. data/docs/integrations.md +124 -0
  9. data/docs/introduction.md +266 -0
  10. data/docs/scheduling.md +176 -0
  11. data/docs/sharing.md +211 -0
  12. data/docs/shims.md +144 -0
  13. data/docs/utilities.md +93 -0
  14. data/docs/variants-and-scopes.md +92 -0
  15. data/lib/farce/abstract/flag.rb +6 -0
  16. data/lib/farce/backports.rb +274 -0
  17. data/lib/farce/engine/jruby/fiber_scheduler.rb +3 -0
  18. data/lib/farce/engine/jruby/scheduler_synchronization.rb +87 -0
  19. data/lib/farce/engine/ruby/3.4/null_port.rb +21 -0
  20. data/lib/farce/engine/ruby/3.4/patch.rb +67 -0
  21. data/lib/farce/engine/ruby/3.4/port.rb +13 -12
  22. data/lib/farce/engine/ruby/3.4/ractor_methods.rb +0 -1
  23. data/lib/farce/engine/ruby/4.0/null_port.rb +36 -0
  24. data/lib/farce/engine/ruby/4.0/port.rb +1 -2
  25. data/lib/farce/engine/ruby/4.1/null_port.rb +5 -0
  26. data/lib/farce/engine/ruby.rb +7 -0
  27. data/lib/farce/engine/shared/null_port.rb +18 -0
  28. data/lib/farce/engine/shared/port.rb +41 -3
  29. data/lib/farce/engine/shared/queue.rb +4 -4
  30. data/lib/farce/engine/shared/ractor_methods.rb +2 -2
  31. data/lib/farce/engine/shared/ractor_selector.rb +73 -0
  32. data/lib/farce/extension.rb +150 -0
  33. data/lib/farce/integrations/ractor_tmvar.rb +4 -1
  34. data/lib/farce/internal/fake_ractor.rb +10 -2
  35. data/lib/farce/internal/scheduler_lifecycle.rb +7 -4
  36. data/lib/farce/null_port.rb +12 -0
  37. data/lib/farce/system.rb +10 -2
  38. data/lib/farce/version.rb +1 -1
  39. data/lib/farce.rb +1 -1
  40. 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)
@@ -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)