async 2.27.0 → 2.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/agent.md +16 -0
- data/context/best-practices.md +188 -0
- data/context/debugging.md +63 -0
- data/context/getting-started.md +177 -0
- data/context/index.yaml +29 -0
- data/context/scheduler.md +109 -0
- data/context/tasks.md +448 -0
- data/context/thread-safety.md +651 -0
- data/lib/async/version.rb +1 -1
- data/readme.md +3 -3
- data.tar.gz.sig +0 -0
- metadata +9 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c419b1abab6e535bd3f68c7de489ad0086e236f54b513d959f0b5d81373039e2
|
4
|
+
data.tar.gz: d995214a8ac8539172ffe669563cdaaca34925b7da97939095d2f48f22628ead
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79cf05bc46450f20fdbf524f7bc1886564b26cadcbf731c3186c3f1dbec93ad51f15bc042591b77a790d0647a768103533b2eae8ff732ef5350e3ad2b866ffe0
|
7
|
+
data.tar.gz: 7663314ae8ad34d6c7d647ce8065268cc91754828d474044e3b2c5a6c80bbc9f8467b12fb90af6d4f3d5db6c0a14cc74735ce32ffa37998b2d9c261c5cc4a876
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/agent.md
CHANGED
@@ -30,6 +30,10 @@ This guide explains how to test and monitor documentation coverage in your Ruby
|
|
30
30
|
|
31
31
|
This guide covers documentation practices and pragmas supported by the Decode gem for documenting Ruby code. These pragmas provide structured documentation that can be parsed and used to generate A...
|
32
32
|
|
33
|
+
#### [Setting Up RBS Types and Steep Type Checking for Ruby Gems](.context/decode/types.md)
|
34
|
+
|
35
|
+
This guide covers the process for establishing robust type checking in Ruby gems using RBS and Steep, focusing on automated generation from source documentation and proper validation.
|
36
|
+
|
33
37
|
### sus
|
34
38
|
|
35
39
|
A fast and scalable test runner.
|
@@ -45,3 +49,15 @@ There are two types of mocking in sus: `receive` and `mock`. The `receive` match
|
|
45
49
|
#### [Shared Test Behaviors and Fixtures](.context/sus/shared.md)
|
46
50
|
|
47
51
|
Sus provides shared test contexts which can be used to define common behaviours or tests that can be reused across one or more test files.
|
52
|
+
|
53
|
+
### sus-fixtures-agent-context
|
54
|
+
|
55
|
+
Test fixtures for running in Async.
|
56
|
+
|
57
|
+
#### [Getting Started](.context/sus-fixtures-agent-context/getting-started.md)
|
58
|
+
|
59
|
+
This guide explains how to use the `sus-fixtures-agent-context` gem to test agent contexts.
|
60
|
+
|
61
|
+
#### [GitHub Actions](.context/sus-fixtures-agent-context/github-actions.md)
|
62
|
+
|
63
|
+
This guide explains how to integrate the `sus-fixtures-agent-context` gem with GitHub Actions for testing agent contexts.
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# Best Practices
|
2
|
+
|
3
|
+
This guide gives an overview of best practices for using Async.
|
4
|
+
|
5
|
+
## Use a top-level `Sync` to denote the root of your program
|
6
|
+
|
7
|
+
`Async{}` has two uses: it creates an event loop if one doesn't exist, and it creates a task which runs asynchronously with respect to the parent scope. However, the top level `Async{}` block will be synchronous because it creates the event loop. In some programs, you do not care about executing asynchronously, but you still want your code to run in an event loop. `Sync{}` exists to do this efficiently.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
require 'async'
|
11
|
+
|
12
|
+
class Packages
|
13
|
+
def initialize(urls)
|
14
|
+
@urls = urls
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch
|
18
|
+
# A common use case is to make functions which appear synchronous, but internally use asynchronous execution:
|
19
|
+
Sync do |task|
|
20
|
+
@urls.map do |url|
|
21
|
+
task.async do
|
22
|
+
fetch(url)
|
23
|
+
end
|
24
|
+
end.map(&:wait)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
`Sync{...}` is semantically equivalent to `Async{}.wait`, but it is more efficient. It is the preferred way to run code in an event loop at the top level of your program or to ensure some code runs in an event loop without creating a new task. The name `Sync` means "Synchronous Async", indicating that it runs synchronously with respect to the outer scope, but still allows for asynchronous execution within it.
|
31
|
+
|
32
|
+
### Current Task
|
33
|
+
|
34
|
+
In some scenarios, it can be invalid to call a method outside of an event loop, for example a top level `Async{...}` can block forever, which might be unexpected.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
def wait(queue)
|
38
|
+
Async do
|
39
|
+
queue.pop
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
You can force callers of a method to only call the method within an asynchronous context by using a keyword argument `parent: Async::Task.current`. If no task is present, this will raise an exception.
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
def wait(queue, parent: Async::Task.current)
|
48
|
+
parent.async do
|
49
|
+
queue.pop
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
This expresses the intent to the caller that this method should only be invoked from within an asynchonous task. In addition, it allows the caller to substitute other parent objects, like semaphores or barriers, which can be useful for managing concurrency.
|
55
|
+
|
56
|
+
## Use barriers to manage unbounded concurrency
|
57
|
+
|
58
|
+
Barriers provide a way to manage an unbounded number of tasks.
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
Async do
|
62
|
+
barrier = Async::Barrier.new
|
63
|
+
|
64
|
+
items.each do |item|
|
65
|
+
barrier.async do
|
66
|
+
process(item)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Process the tasks in order of completion:
|
71
|
+
barrier.wait do |task|
|
72
|
+
result = task.wait
|
73
|
+
# Do something with result.
|
74
|
+
|
75
|
+
# If you don't want to wait for any more tasks you can break:
|
76
|
+
break
|
77
|
+
end
|
78
|
+
|
79
|
+
# Or just wait for all tasks to finish:
|
80
|
+
barrier.wait # May raise an exception if a task failed.
|
81
|
+
ensure
|
82
|
+
# Stop all outstanding tasks in the barrier:
|
83
|
+
barrier&.stop
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
## Use a semaphore to limit the number of concurrent tasks
|
88
|
+
|
89
|
+
Semaphores allow you to limit the level of concurrency to a fixed number of tasks:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
Async do |task|
|
93
|
+
barrier = Async::Barrier.new
|
94
|
+
semaphore = Async::Semaphore.new(4, parent: barrier)
|
95
|
+
|
96
|
+
# Since the semaphore.async may block, we need to run the work scheduling in a child task:
|
97
|
+
task.async do
|
98
|
+
items.each do |item|
|
99
|
+
semaphore.async do
|
100
|
+
process(item)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Wait for all the work to complete:
|
106
|
+
barrier.wait
|
107
|
+
ensure
|
108
|
+
# Stop all outstanding tasks in the barrier:
|
109
|
+
barrier&.stop
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
In general, the barrier should be the root of your task hierarchy, and the semaphore should be a child of the barrier. This allows you to manage the lifetime of all tasks created by the semaphore, and ensures that all tasks are stopped when the barrier is stopped.
|
114
|
+
|
115
|
+
### Idler
|
116
|
+
|
117
|
+
Idlers are like semaphores but with a limit defined by current processor utilization. In other words, an idler will do work up to a specific ratio of idle/busy time in the scheduler, and try to maintain that.
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
Async do
|
121
|
+
# Create an idler that will aim for a load average of 80%:
|
122
|
+
idler = Async::Idler.new(0.8)
|
123
|
+
|
124
|
+
# Some list of work to be done:
|
125
|
+
work.each do |work|
|
126
|
+
idler.async do
|
127
|
+
# Do the work:
|
128
|
+
work.call
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
The idler will try to schedule as much work such that the load of the scheduler stays at around 80% saturation.
|
135
|
+
|
136
|
+
## Use queues to share data between tasks
|
137
|
+
|
138
|
+
Queues allow you to share data between tasks without the risk of data corruption or deadlocks.
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
Async do |task|
|
142
|
+
queue = Async::Queue.new
|
143
|
+
|
144
|
+
reader = task.async do
|
145
|
+
while chunk = socket.gets
|
146
|
+
queue.push(chunk)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
# After this point, we won't be able to add items to the queue, and popping items will eventually result in nil once all items are dequeued:
|
150
|
+
queue.close
|
151
|
+
end
|
152
|
+
|
153
|
+
# Process items from the queue:
|
154
|
+
while line = queue.pop
|
155
|
+
process(line)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
The above program may have unbounded memory use, so it can be a good idea to use a limited queue with back-pressure:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
Async do |task|
|
164
|
+
queue = Async::LimitedQueue.new(8)
|
165
|
+
|
166
|
+
# Everything else is the same from the queue example, except that the pushing onto the queue will block once 8 items are buffered.
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
## Use timeouts for operations that might block forever
|
171
|
+
|
172
|
+
General timeouts can be imposed by using `task.with_timeout(duration)`.
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
Async do |task|
|
176
|
+
# This will raise an Async::TimeoutError after 1 second:
|
177
|
+
task.with_timeout(1) do |timeout|
|
178
|
+
# Timeout#duration= can be used to adjust the duration of the timeout.
|
179
|
+
# Timeout#cancel can be used to cancel the timeout completely.
|
180
|
+
|
181
|
+
sleep 10
|
182
|
+
end
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
It can be especially important to impose timeouts when processing user-provided data.
|
187
|
+
|
188
|
+
##
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# Debugging
|
2
|
+
|
3
|
+
This guide explains how to debug issues with programs that use Async.
|
4
|
+
|
5
|
+
## Debugging Techniques
|
6
|
+
|
7
|
+
### Debugging with `puts`
|
8
|
+
|
9
|
+
The simplest way to debug an Async program is to use `puts` to print messages to the console. This is useful for understanding the flow of your program and the values of variables. However, it can be difficult to use `puts` to debug programs that use asynchronous code, as the output may be interleaved. To prevent this, wrap it in `Fiber.blocking{}`:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require 'async'
|
13
|
+
|
14
|
+
Async do
|
15
|
+
3.times do |i|
|
16
|
+
sleep i
|
17
|
+
Fiber.blocking{puts "Slept for #{i} seconds."}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
Using `Fiber.blocking{}` prevents any context switching until the block is complete, ensuring that the output is not interleaved and that flow control is strictly sequential. You should not use `Fiber.blocking{}` in production code, as it will block the reactor.
|
23
|
+
|
24
|
+
### Debugging with IRB
|
25
|
+
|
26
|
+
You can use IRB to debug your Async program. In some cases, you will want to stop the world and inspect the state of your program. You can do this by wrapping `binding.irb` inside a `Fiber.blocking{}` block:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
Async do
|
30
|
+
3.times do |i|
|
31
|
+
sleep i
|
32
|
+
# The event loop will stop at this point and you can inspect the state of your program.
|
33
|
+
Fiber.blocking{binding.irb}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
If you don't use `Fiber.blocking{}`, the event loop will continue to run and you will end up with three instances of `binding.irb` running.
|
39
|
+
|
40
|
+
### Debugging with `Async::Debug`
|
41
|
+
|
42
|
+
The `async-debug` gem provides a visual debugger for Async programs. It is a powerful tool that allows you to inspect the state of your program and see the hierarchy of your program:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
require 'async'
|
46
|
+
require 'async/debug'
|
47
|
+
|
48
|
+
Sync do
|
49
|
+
debugger = Async::Debug.serve
|
50
|
+
|
51
|
+
3.times do
|
52
|
+
Async do |task|
|
53
|
+
while true
|
54
|
+
duration = rand
|
55
|
+
task.annotate("Sleeping for #{duration} second...")
|
56
|
+
sleep(duration)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
When you run this program, it will start a web server on `http://localhost:9000`. You can open this URL in your browser to see the state of your program.
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# Getting Started
|
2
|
+
|
3
|
+
This guide shows how to add async to your project and run code asynchronously.
|
4
|
+
|
5
|
+
Async is a Ruby library that provides asynchronous programming capabilities using fibers and a fiber scheduler. It allows you to write non-blocking, concurrent code that's easy to understand and maintain.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add the gem to your project:
|
10
|
+
|
11
|
+
~~~ bash
|
12
|
+
$ bundle add async
|
13
|
+
~~~
|
14
|
+
|
15
|
+
## Core Concepts
|
16
|
+
|
17
|
+
`async` has several core concepts:
|
18
|
+
|
19
|
+
- A {ruby Async::Task} instance which captures your sequential computations.
|
20
|
+
- A {ruby Async::Reactor} instance which implements the fiber scheduler interface and event loop.
|
21
|
+
- A {ruby Fiber} is an object which executes user code with cooperative concurrency, i.e. you can transfer execution from one fiber to another and back again.
|
22
|
+
|
23
|
+
### What is a scheduler?
|
24
|
+
|
25
|
+
A scheduler is an interface which manages the execution of fibers. It is responsible for intercepting blocking operations and redirecting them to an event loop.
|
26
|
+
|
27
|
+
### What is an event loop?
|
28
|
+
|
29
|
+
An event loop is part of the implementation of a scheduler which is responsible for waiting for events to occur, and waking up fibers when they are ready to run.
|
30
|
+
|
31
|
+
### What is a selector?
|
32
|
+
|
33
|
+
A selector is part of the implementation of an event loop which is responsible for interacting with the operating system and waiting for specific events to occur. This is often referred to as "select"ing ready events from a set of file descriptors, but in practice has expanded to encompass a wide range of blocking operations.
|
34
|
+
|
35
|
+
### What is a reactor?
|
36
|
+
|
37
|
+
A reactor is a specific implementation of the scheduler interface, which includes an event loop and selector, and is responsible for managing the execution of fibers.
|
38
|
+
|
39
|
+
## Creating an Asynchronous Task
|
40
|
+
|
41
|
+
The main entry point for creating tasks is the {ruby Kernel#Async} method. Because this method is defined on `Kernel`, it's available in all parts of your program.
|
42
|
+
|
43
|
+
~~~ ruby
|
44
|
+
require 'async'
|
45
|
+
|
46
|
+
Async do |task|
|
47
|
+
puts "Hello World!"
|
48
|
+
end
|
49
|
+
~~~
|
50
|
+
|
51
|
+
A {ruby Async::Task} runs using a {ruby Fiber} and blocking operations e.g. `sleep`, `read`, `write` yield control until the operation can complete. When a blocking operation yields control, it means another fiber can execute, giving the illusion of simultaneous execution.
|
52
|
+
|
53
|
+
### When should I use `Async`?
|
54
|
+
|
55
|
+
You should use `Async` when you desire explicit concurrency in your program. That means you want to run multiple tasks at the same time, and you want to be able to wait for the results of those tasks.
|
56
|
+
|
57
|
+
- You should use `Async` when you want to perform network operations concurrently, such as HTTP requests or database queries.
|
58
|
+
- You should use `Async` when you want to process independent requests concurrently, such as a web server.
|
59
|
+
- You should use `Async` when you want to handle multiple connections concurrently, such as a chat server.
|
60
|
+
|
61
|
+
You should consider the boundary around your program and the request handling. For example, one task per operation, request or connection, is usually appropriate.
|
62
|
+
|
63
|
+
### Waiting for Results
|
64
|
+
|
65
|
+
Similar to a promise, {ruby Async::Task} produces results. In order to wait for these results, you must invoke {ruby Async::Task#wait}:
|
66
|
+
|
67
|
+
``` ruby
|
68
|
+
require 'async'
|
69
|
+
|
70
|
+
task = Async do
|
71
|
+
rand
|
72
|
+
end
|
73
|
+
|
74
|
+
puts "The number was: #{task.wait}"
|
75
|
+
```
|
76
|
+
|
77
|
+
## Creating a Fiber Scheduler
|
78
|
+
|
79
|
+
The first (top level) async block will also create an instance of {ruby Async::Reactor} which is a subclass of {ruby Async::Scheduler} to handle the event loop. You can also do this directly using {ruby Fiber.set_scheduler}:
|
80
|
+
|
81
|
+
~~~ ruby
|
82
|
+
require 'async/scheduler'
|
83
|
+
|
84
|
+
scheduler = Async::Scheduler.new
|
85
|
+
Fiber.set_scheduler(scheduler)
|
86
|
+
|
87
|
+
Fiber.schedule do
|
88
|
+
1.upto(3) do |i|
|
89
|
+
Fiber.schedule do
|
90
|
+
sleep 1
|
91
|
+
puts "Hello World"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
~~~
|
96
|
+
|
97
|
+
## Synchronous Execution in an existing Fiber Scheduler
|
98
|
+
|
99
|
+
Unless you need fan-out, map-reduce style concurrency, you can actually use a slightly more efficient {ruby Kernel::Sync} execution model. This method will run your block in the current event loop if one exists, or create an event loop if not. You can use it for code which uses asynchronous primitives, but itself does not need to be asynchronous with respect to other tasks.
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
require 'async/http/internet'
|
103
|
+
|
104
|
+
def fetch(url)
|
105
|
+
Sync do
|
106
|
+
internet = Async::HTTP::Internet.new
|
107
|
+
return internet.get(url).read
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# At the level of your program, this method will create an event loop:
|
112
|
+
fetch(...)
|
113
|
+
|
114
|
+
Sync do
|
115
|
+
# The event loop already exists, and will be reused:
|
116
|
+
fetch(...)
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
In other words, `Sync{...}` is very similar in behaviour to `Async{...}.wait`, but significantly more efficient.
|
121
|
+
|
122
|
+
## Enforcing Embedded Execution
|
123
|
+
|
124
|
+
In some methods, you may want to implement a fan-out or map-reduce. That requires a parent scheduler. There are two ways you can do this:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
def fetch_all(urls, parent: Async::Task.current)
|
128
|
+
urls.map do |url|
|
129
|
+
parent.async do
|
130
|
+
fetch(url)
|
131
|
+
end
|
132
|
+
end.map(&:wait)
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
or:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
def fetch_all(urls)
|
140
|
+
Sync do |parent|
|
141
|
+
urls.map do |url|
|
142
|
+
parent.async do
|
143
|
+
fetch(url)
|
144
|
+
end
|
145
|
+
end.map(&:wait)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
The former allows you to inject the parent, which could be a barrier or semaphore, while the latter will create a new parent scheduler if one does not exist. In both cases, you guarantee that the map operation will be executed in the parent task (of some sort).
|
151
|
+
|
152
|
+
## Compatibility
|
153
|
+
|
154
|
+
The Fiber Scheduler interface is compatible with most pure Ruby code and well-behaved C code. For example, you can use {ruby Net::HTTP} for performing concurrent HTTP requests:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
urls = [...]
|
158
|
+
|
159
|
+
Async do
|
160
|
+
# Perform several concurrent requests:
|
161
|
+
responses = urls.map do |url|
|
162
|
+
Async do
|
163
|
+
Net::HTTP.get(url)
|
164
|
+
end
|
165
|
+
end.map(&:wait)
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
Unfortunately, some libraries do not integrate well with the fiber scheduler: either they are blocking, processor bound, or use thread locals for execution state. To use these libraries, you may be able to use a background thread.
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
Async do
|
173
|
+
result = Thread.new do
|
174
|
+
# Code which is otherwise unsafe...
|
175
|
+
end.value # Wait for the result of the thread, internally non-blocking.
|
176
|
+
end
|
177
|
+
```
|
data/context/index.yaml
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Automatically generated context index for Utopia::Project guides.
|
2
|
+
# Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
|
3
|
+
---
|
4
|
+
getting-started:
|
5
|
+
title: Getting Started
|
6
|
+
order: 1
|
7
|
+
description: This guide shows how to add async to your project and run code asynchronously.
|
8
|
+
scheduler:
|
9
|
+
title: Scheduler
|
10
|
+
order: 2
|
11
|
+
description: This guide gives an overview of how the scheduler is implemented.
|
12
|
+
tasks:
|
13
|
+
title: Asynchronous Tasks
|
14
|
+
order: 3
|
15
|
+
description: This guide explains how asynchronous tasks work and how to use them.
|
16
|
+
best-practices:
|
17
|
+
title: Best Practices
|
18
|
+
order: 5
|
19
|
+
description: This guide gives an overview of best practices for using Async.
|
20
|
+
debugging:
|
21
|
+
title: Debugging
|
22
|
+
order: 6
|
23
|
+
description: This guide explains how to debug issues with programs that use Async.
|
24
|
+
thread-safety:
|
25
|
+
title: Thread safety
|
26
|
+
order: 10
|
27
|
+
description: This guide explains thread safety in Ruby, focusing on fibers and threads,
|
28
|
+
common pitfalls, and best practices to avoid problems like data corruption, race
|
29
|
+
conditions, and deadlocks.
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# Scheduler
|
2
|
+
|
3
|
+
This guide gives an overview of how the scheduler is implemented.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The {ruby Async::Scheduler} uses an event loop to execute tasks. When tasks are waiting on blocking operations like IO, the scheduler will use the operating system's native event system to wait for the operation to complete. This allows the scheduler to efficiently handle many tasks.
|
8
|
+
|
9
|
+
### Tasks
|
10
|
+
|
11
|
+
Tasks are the building blocks of concurrent programs. They are lightweight and can be scheduled by the event loop. Tasks can be nested, and the parent task is used to determine the current reactor. Tasks behave like promises, in the sense you can wait on them to complete, and they might fail with an exception.
|
12
|
+
|
13
|
+
~~~ ruby
|
14
|
+
require 'async'
|
15
|
+
|
16
|
+
def sleepy(duration, task: Async::Task.current)
|
17
|
+
task.async do |subtask|
|
18
|
+
subtask.annotate "I'm going to sleep #{duration}s..."
|
19
|
+
sleep duration
|
20
|
+
puts "I'm done sleeping!"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def nested_sleepy(task: Async::Task.current)
|
25
|
+
task.async do |subtask|
|
26
|
+
subtask.annotate "Invoking sleepy 5 times..."
|
27
|
+
5.times do |index|
|
28
|
+
sleepy(index, task: subtask)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Async do |task|
|
34
|
+
task.annotate "Invoking nested_sleepy..."
|
35
|
+
subtask = nested_sleepy
|
36
|
+
|
37
|
+
# Print out all running tasks in a tree:
|
38
|
+
task.print_hierarchy($stderr)
|
39
|
+
|
40
|
+
# Kill the subtask
|
41
|
+
subtask.stop
|
42
|
+
end
|
43
|
+
~~~
|
44
|
+
|
45
|
+
### Thread Safety
|
46
|
+
|
47
|
+
Most methods of the reactor and related tasks are not thread-safe, so you'd typically have [one reactor per thread or process](https://github.com/socketry/async-container).
|
48
|
+
|
49
|
+
### Embedding Schedulers
|
50
|
+
|
51
|
+
{ruby Async::Scheduler#run} will run until the reactor runs out of work to do. To run a single iteration of the reactor, use {ruby Async::Scheduler#run_once}.
|
52
|
+
|
53
|
+
~~~ ruby
|
54
|
+
require 'async'
|
55
|
+
|
56
|
+
Console.logger.debug!
|
57
|
+
reactor = Async::Scheduler.new
|
58
|
+
|
59
|
+
# Run the reactor for 1 second:
|
60
|
+
reactor.async do |task|
|
61
|
+
sleep 1
|
62
|
+
puts "Finished!"
|
63
|
+
end
|
64
|
+
|
65
|
+
while reactor.run_once
|
66
|
+
# Round and round we go!
|
67
|
+
end
|
68
|
+
~~~
|
69
|
+
|
70
|
+
You can use this approach to embed the reactor in another event loop. For some integrations, you may want to specify the maximum time to wait to {ruby Async::Scheduler#run_once}.
|
71
|
+
|
72
|
+
### Stopping a Scheduler
|
73
|
+
|
74
|
+
{ruby Async::Scheduler#stop} will stop the current scheduler and all children tasks.
|
75
|
+
|
76
|
+
### Fiber Scheduler Integration
|
77
|
+
|
78
|
+
In order to integrate with native Ruby blocking operations, the {ruby Async::Scheduler} uses a {ruby Fiber::Scheduler} interface.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
require 'async'
|
82
|
+
|
83
|
+
scheduler = Async::Scheduler.new
|
84
|
+
Fiber.set_scheduler(scheduler)
|
85
|
+
|
86
|
+
Fiber.schedule do
|
87
|
+
puts "Hello World!"
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
## Design
|
92
|
+
|
93
|
+
### Optimistic vs Pessimistic Scheduling
|
94
|
+
|
95
|
+
There are two main strategies for scheduling tasks: optimistic and pessimistic. An optimistic scheduler is usually greedy and will try to execute tasks as soon as they are scheduled using a direct transfer of control flow. A pessimistic scheduler will schedule tasks into the event loop ready list and will only execute them on the next iteration of the event loop.
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
Async do
|
99
|
+
puts "Hello "
|
100
|
+
|
101
|
+
Async do
|
102
|
+
puts "World"
|
103
|
+
end
|
104
|
+
|
105
|
+
puts "!"
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
An optimstic scheduler will print "Hello World!", while a pessimistic scheduler will print "Hello !World". In practice you should not design your code to rely on the order of execution, but it's important to understand the difference. It is an unspecifed implementation detail of the scheduler.
|