10000 Documentation improvements. (#248) · socketry/async@d28262f · GitHub
[go: up one dir, main page]

Skip to content

Commit d28262f

Browse files
authored
Documentation improvements. (#248)
1 parent 576b2b0 commit d28262f

File tree

1 file changed

+55
-31
lines changed

1 file changed

+55
-31
lines changed

guides/asynchronous-tasks/readme.md

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ This guide explains how asynchronous tasks work and how to use them.
44

55
## Overview
66

7-
Tasks are the smallest unit of sequential code execution in {ruby Async}. Tasks can create other tasks, and Async tracks the parent-child relationship between tasks. When a parent task is stopped, it will also stop all its children tasks. The outer event loop generally has one root task.
7+
Tasks are the smallest unit of sequential code execution in {ruby Async}. Tasks can create other tasks, and Async tracks the parent-child relationship between tasks. When a parent task is stopped, it will also stop all its children tasks. The outer reactor (event loop) always starts with one root task.
88

99
```mermaid
1010
graph LR
11-
EL[Event Loop] --> WS
11+
REL[Reactor Event Loop] --> WS
1212
WS[Web Server Task] --> R1[Request 1 Task]
1313
WS --> R2[Request 2 Task]
1414
@@ -28,7 +28,7 @@ stateDiagram-v2
2828
[*] --> initialized : Task.new
2929
initialized --> running : run
3030
31-
running --> failed : unhandled exception
31+
running --> failed : unhandled StandardError-derived exception
3232
running --> complete : user code finished
3333
running --> stopped : stop
3434
@@ -39,10 +39,10 @@ stateDiagram-v2
3939
stopped --> [*]
4040
```
4141

42-
Tasks are created in the `initialized` state, and are run by the event loop. During the execution, a task can either `complete` successfully, become `failed` with an unhandled exception, or be explicitly `stopped`. In all of these cases, you can wait for a task to complete by using {ruby Async::Task#wait}.
42+
Tasks are created in the `initialized` state, and are run by the reactor. During the execution, a task can either `complete` successfully, become `failed` with an unhandled `StandardError`-derived exception, or be explicitly `stopped`. In all of these cases, you can wait for a task to complete by using {ruby Async::Task#wait}.
4343

4444
1. In the case the task successfully completed, the result will be whatever value was generated by the last expression in the task.
45-
2. In the case the task failed with an unhandled exception, waiting on the task will re-raise the exception.
45+
2. In the case the task failed with an unhandled `StandardError`-derived exception, waiting on the task will re-raise the exception.
4646
3. In the case the task was stopped, the result will be `nil`.
4747

4848
## Starting A Task
@@ -52,32 +52,32 @@ At any point in your program, you can start the reactor and a root task using th
5252
```ruby
5353
Async do
5454
3.times do |i|
55-
sleep 1
55+
sleep(i)
5656
puts "Hello World #{i}"
5757
end
5858
end
5959
```
6060

61-
This program prints "Hello World" 3 times. Before printing, it sleeps for 1 second. The total execution time is 3 seconds because the program executes sequentially.
61+
This program prints "Hello World" 3 times. Before printing, it sleeps for 1, then 2, then 3 seconds. The total execution time is 6 seconds because the program executes sequentially.
6262

6363
By using a nested task, we can ensure that each iteration of the loop creates a new task which runs concurrently.
6464

6565
```ruby
6666
Async do
6767
3.times do |i|
6868
Async do
69-
sleep 1
69+
sleep(i)
7070
puts "Hello World #{i}"
7171
end
7272
end
7373
end
7474
```
7575

76-
Instead of taking 3 seconds, this program takes 1 second in total. The main loop executes rapidly creating 3 child tasks, and then each child task sleeps for 1 second before printing "Hello World".
76+
Instead of taking 6 seconds, this program takes 3 seconds in total. The main loop executes rapidly creating 3 child tasks, and then each child task sleeps for 1, 2 and 3 seconds respectively before printing "Hello World".
7777

7878
```mermaid
7979
graph LR
80-
EL[Event Loop] --> TT[Initial Task]
80+
REL[Reactor Event Loop] --> TT[Initial Task]
8181
8282
TT --> H0[Hello World 0 Task]
8383
TT --> H1[Hello World 1 Task]
@@ -185,19 +185,19 @@ barrier.wait
185185

186186
When a task completes execution, it will enter the `complete` state (or the `failed` state if it raises an unhandled exception).
187187

188-
There are various situations where you may want to stop a task ({ruby Async::Task#stop}) before it completes. The most common case is shutting down a server, but other important situations exist, e.g. you may fan out multiple (10s, 100s) of requests, wait for a subset to complete (e.g. the first 5 or all those that complete within a given deadline), and then stop (terminate/cancel) the remaining operations.
188+
There are various situations where you may want to stop a task ({ruby Async::Task#stop}) before it completes. The most common case is shutting down a server. A more complex example is this: you may fan out multiple (10s, 100s) of requests, wait for a subset to complete (e.g. the first 5 or all those that complete within a given deadline), and then stop (terminate/cancel) the remaining operations.
189189

190-
Using the above program as an example, we can
190+
Using the above program as an example, let's stop all the tasks just after the first one completes.
191191

192192
```ruby
193193
Async do
194194
tasks = 3.times.map do |i|
195195
Async do
196-
sleep 1
196+
sleep(i)
197197
puts "Hello World #{i}"
198198
end
199199
end
200-
200+
201201
# Stop all the above tasks:
202202
tasks.each(&:stop)
203203
end
@@ -213,7 +213,7 @@ barrier = Async::Barrier.new
213213
Async do
214214
tasks = 3.times.map do |i|
215215
barrier.async do
216-
sleep 1
216+
sleep(i)
217217
puts "Hello World #{i}"
218218
end
219219
end
@@ -222,15 +222,15 @@ Async do
222222
end
223223
```
224224

225-
If you're letting individual tasks held by a barrier raise unhandled exceptions, be sure to call ({ruby Async::Barrier#stop}) to stop the remaining tasks:
225+
Unless your tasks all rescue and suppresses `StandardError`-derived exceptions, be sure to call ({ruby Async::Barrier#stop}) to stop the remaining tasks:
226226

227227
```ruby
228228
barrier = Async::Barrier.new
229229

230230
Async do
231231
tasks = 3.times.map do |i|
232232
barrier.async do
233-
sleep 1
233+
sleep(i)
234234
puts "Hello World #{i}"
235235
end
236236
end
@@ -264,7 +264,7 @@ As tasks run synchronously until they yield back to the reactor, you can guarant
264264

265265
## Exception Handling
266266

267-
{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.
267+
{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 cause the reactor to exit. This ensures that exceptions will always be visible and cause the program to fail appropriately.
268268

269269
~~~ ruby
270270
require 'async'
@@ -307,7 +307,7 @@ require 'async'
307307

308308
Async do |task|
309309
task.with_timeout(1) do
310-
sleep 100
310+
sleep(100)
311311
rescue Async::TimeoutError
312312
puts "I timed out 99 seconds early!"
313313
end
@@ -321,11 +321,33 @@ Sometimes you need to do some recurring work in a loop. Often it's best to measu
321321
~~~ ruby
322322
require 'async'
323323

324+
period = 30
325+
324326
Async do |task|
325327
loop do
326328
puts Time.now
327329
# ... process job ...
328-
sleep 1
330+
sleep(period)
331+
end
332+
end
333+
~~~
334+
335+
If you need a periodic timer that runs start-to-start, you can keep track of the `run_next` time using the monotonic clock:
336+
337+
~~~ ruby
338+
require 'async'
339+
340+
period = 30
341+
342+
Async do |task|
343+
run_next = Async::Clock.now
344+
loop do
345+
run_next += period
346+
puts Time.now
347+
# ... process job ...
348+
if (remaining = run_next - Async::Clock.now) > 0
349+
sleep(remaining)
350+
end
329351
end
330352
end
331353
~~~
@@ -340,9 +362,8 @@ Tasks which are flagged as `transient` do not behave like normal tasks.
340362

341363
```ruby
342364
@pruner = Async(transient: true) do
343-
loop do
344-
345-
sleep 1
365+
loop do
366+
sleep(1)
346367
prune_connection_pool
347368
end
348369
end
@@ -359,43 +380,46 @@ The purpose of transient tasks is when a task is an implementation detail of an
359380
- A background worker or batch processing job which is independent of any specific operation, and is lazily created.
360381
- A cache system which needs periodic expiration / revalidation of data/values.
361382

362-
Bearing in mind, in all of the above cases, you may need to validate that the background task hasn't been stopped, e.g.
383+
Here is an example that keeps a cache of the current time string since that has only 1-second granularity
384+
and you could be handling 1000s of requests per second.
385+
The task doing the updating in the background is an implementation detail, so it is marked as `transient`.
363386

364387
```ruby
365388
require 'async'
366389
require 'thread/local' # thread-local gem.
367390

368-
class TimeCache
391+
class TimeStringCache
369392
extend Thread::Local # defines `instance` class method that lazy-creates a separate instance per thread
370393

371394
def initialize
372-
@current_time = nil
395+
@current_time_string = nil
373396
end
374397

375398
def current_time_string
376399
refresh!
377400

378-
return @current_time
401+
return @current_time_string
379402
end
380403

381404
private
382405

383406
def refresh!
384407
@refresh ||= Async(transient: true) do
385408
loop do
386-
@current_time = Time.now.to_s
409+
@current_time_string = Time.now.to_s
387410
sleep(1)
388411
end
389412
ensure
390-
# When the reactor terminates all tasks, this will be invoked:
413+
# When the reactor terminates all tasks, `Async::Stop` will be raised from `sleep` and
414+
# this code will be invoked.
415+
# By clearing `@refresh`, we ensure that the task will be recreated if needed again:
391416
@refresh = nil
392417
end
393418
end
394419
end
395420

396421
Async do
397-
# If you are handling 1000s of requests per second, it can be an advantage to cache the current time string.
398-
p TimeCache.instance.current_time_string
422+
p TimeStringCache.instance.current_time_string
399423
end
400424
```
401425

0 commit comments

Comments
 (0)
0