1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
|
# Getting Started
This guide explains how to use `async` for event-driven systems.
## Installation
Add the gem to your project:
~~~ bash
$ bundle add async
~~~
## Core Concepts
`async` has several core concepts:
- A {ruby Async::Task} instance which captures your sequential computations.
- A {ruby Async::Reactor} instance which implements the core event loop.
## Creating Tasks
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 areas of your program.
~~~ ruby
require 'async'
Async do |task|
puts "Hello World!"
end
~~~
An {ruby Async::Task} runs using a {ruby Fiber} and blocking operations e.g. `sleep`, `read`, `write` yield control until the operation can complete.
At the top level, `Async do ... end` will create an event loop, and nested `Async` blocks will reuse the existing event loop. This allows the caller to have either blocking or non-blocking behaviour.
~~~ ruby
require 'async'
def sleepy(duration = 1)
Async do |task|
task.sleep duration
puts "I'm done sleeping, time for action!"
end
end
# Synchronous operation:
sleepy
# Asynchronous operation:
Async do
# These two functions will sleep simultaneously.
sleepy
sleepy
end
~~~
If you want to guarantee synchronous execution, you can use {ruby Kernel#Sync} which is semantically identical to `Async` except that in all cases it will wait until the given block completes execution.
### Nested Tasks
Sometimes it's convenient to explicitly nest tasks. There are a variety of reasons to do this, including grouping tasks in order to wait for completion. In the most basic case, you can make a child task using the {ruby Async::Task#async} method.
~~~ ruby
require 'async'
def nested_sleepy(task: Async::Task.current)
# Block caller
task.sleep 0.1
# Schedule nested task:
subtask = task.async(annotation: "Sleeping") do |subtask|
puts "I'm going to sleep..."
subtask.sleep 1.0
ensure
puts "I'm waking up!"
end
end
Async(annotation: "Top Level") do |task|
subtask = nested_sleepy(task: task)
task.reactor.print_hierarchy
#<Async::Reactor:0x64 1 children (running)>
#<Async::Task:0x78 Top Level (running)>
#<Async::Task:0x8c Sleeping (running)>
end
~~~
This example creates a child `subtask` from the given parent `task`. It's the most efficient way to schedule a task. The task is executed until the first blocking operation, at which point it will yield control and `#async` will return. The result of this method is the task itself.
## Waiting For Results
Like promises, {ruby Async::Task} produces results. In order to wait for these results, you must invoke {ruby Async::Task#wait}:
~~~ ruby
require 'async'
task = Async do
rand
end
puts task.wait
~~~
### Waiting For Multiple Tasks
You can use {ruby Async::Barrier#async} to create multiple child tasks, and wait for them all to complete using {ruby Async::Barrier#wait}.
{ruby Async::Barrier} and {ruby Async::Semaphore} are designed to be compatible with each other, and with other tasks that nest `#async` invocations. There are other similar situations where you may want to pass in a parent task, e.g. {ruby Async::IO::Endpoint#bind}.
~~~ ruby
barrier = Async::Barrier.new
semaphore = Async::Semaphore.new(2)
semaphore.async(parent: barrier) do
# ...
end
~~~
A `parent:` in this context is anything that responds to `#async` in the same way that {ruby Async::Task} responds to `#async`. In situations where you strictly depend on the interface of {ruby Async::Task}, use the `task: Task.current` pattern.
### Stopping Tasks
Use {ruby Async::Task#stop} to stop tasks. This function raises {ruby Async::Stop} on the target task and all descendent tasks.
~~~ ruby
require 'async'
Async do
sleepy = Async do |task|
task.sleep 1000
end
sleepy.stop
end
~~~
When you design a server, you should return the task back to the caller. They can use this task to stop the server if needed, independently of any other unrelated tasks within the reactor, and it will correctly clean up all related tasks.
## Resource Management
In order to ensure your resources are cleaned up correctly, make sure you wrap resources appropriately, e.g.:
~~~ ruby
Async::Reactor.run do
begin
socket = connect(remote_address) # May raise Async::Stop
socket.write(...) # May raise Async::Stop
socket.read(...) # May raise Async::Stop
ensure
socket.close if socket
end
end
~~~
As tasks run synchronously until they yield back to the reactor, you can guarantee this model works correctly. While in theory `IO#autoclose` allows you to automatically close file descriptors when they go out of scope via the GC, it may produce unpredictable behavour (exhaustion of file descriptors, flushing data at odd times), so it's not recommended.
## Exception Handling
{ruby Async::Task} captures and logs exceptions. All unhandled exceptions will cause the enclosing task to enter the `:failed` state. Non-`StandardError` exceptions are re-raised immediately and will generally cause the reactor to fail. This ensures that exceptions will always be visible and cause the program to fail appropriately.
~~~ ruby
require 'async'
task = Async do
# Exception will be logged and task will be failed.
raise "Boom"
end
puts task.status # failed
puts task.result # raises RuntimeError: Boom
~~~
### Propagating Exceptions
If a task has finished due to an exception, calling `Task#wait` will re-raise the exception.
~~~ ruby
require 'async'
Async do
task = Async do
raise "Boom"
end
begin
task.wait # Re-raises above exception.
rescue
puts "It went #{$!}!"
end
end
~~~
## Timeouts
You can wrap asynchronous operations in a timeout. This ensures that malicious services don't cause your code to block indefinitely.
~~~ ruby
require 'async'
Async do |task|
task.with_timeout(1) do
task.sleep 100
rescue Async::TimeoutError
puts "I timed out!"
end
end
~~~
### Reoccurring Timers
Sometimes you need to do some periodic work in a loop.
~~~ ruby
require 'async'
Async do |task|
while true
puts Time.now
task.sleep 1
end
end
~~~
## Caveats
### Enumerators
Due to limitations within Ruby and the nature of this library, it is not possible to use `to_enum` on methods which invoke asynchronous behaviour. We hope to [fix this issue in the future](https://github.com/socketry/async/issues/23).
### Blocking Methods in Standard Library
Blocking Ruby methods such as `pop` in the `Queue` class require access to their own threads and will not yield control back to the reactor which can result in a deadlock. As a substitute for the standard library `Queue`, the {ruby Async::Queue} class can be used.
|