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
|
# Getting Started
This guide shows how to add async to your project and run code asynchronously.
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.
## 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 fiber scheduler interface and event loop.
- 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.
### What is a scheduler?
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.
### What is an event loop?
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.
### What is a selector?
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.
### What is a reactor?
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.
## Creating an Asynchronous Task
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.
~~~ ruby
require 'async'
Async do |task|
puts "Hello World!"
end
~~~
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.
### When should I use `Async`?
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.
- You should use `Async` when you want to perform network operations concurrently, such as HTTP requests or database queries.
- You should use `Async` when you want to process independent requests concurrently, such as a web server.
- You should use `Async` when you want to handle multiple connections concurrently, such as a chat server.
You should consider the boundary around your program and the request handling. For example, one task per operation, request or connection, is usually appropriate.
### Waiting for Results
Similar to a promise, {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 "The number was: #{task.wait}"
```
## Creating a Fiber Scheduler
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}:
~~~ ruby
require 'async/scheduler'
scheduler = Async::Scheduler.new
Fiber.set_scheduler(scheduler)
Fiber.schedule do
1.upto(3) do |i|
Fiber.schedule do
sleep 1
puts "Hello World"
end
end
end
~~~
## Synchronous Execution in an existing Fiber Scheduler
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.
```ruby
require "async/http/internet"
def fetch(url)
Sync do
internet = Async::HTTP::Internet.new
return internet.get(url).read
end
end
# At the level of your program, this method will create an event loop:
fetch("https://example.com")
Sync do
# The event loop already exists, and will be reused:
fetch("https://example.com")
end
```
In other words, `Sync{...}` is very similar in behaviour to `Async{...}.wait`, but significantly more efficient.
## Enforcing Embedded Execution
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:
```ruby
def fetch_all(urls, parent: Async::Task.current)
urls.map do |url|
parent.async do
fetch(url)
end
end.map(&:wait)
end
```
or:
```ruby
def fetch_all(urls)
Sync do |parent|
urls.map do |url|
parent.async do
fetch(url)
end
end.map(&:wait)
end
end
```
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).
## Compatibility
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:
```ruby
urls = ["http://example.com", "http://example.org", "http://example.net"]
Async do
# Perform several concurrent requests:
responses = urls.map do |url|
Async do
Net::HTTP.get(URI(url))
end
end.map(&:wait)
end
```
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.
```ruby
Async do
result = Thread.new do
# Code which is otherwise unsafe...
end.value # Wait for the result of the thread, internally non-blocking.
end
```
|