You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: guides/asynchronous-tasks/readme.md
+55-31Lines changed: 55 additions & 31 deletions
Original file line number
Diff line number
Diff line change
@@ -4,11 +4,11 @@ This guide explains how asynchronous tasks work and how to use them.
4
4
5
5
## Overview
6
6
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.
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}.
43
43
44
44
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.
46
46
3. In the case the task was stopped, the result will be `nil`.
47
47
48
48
## Starting A Task
@@ -52,32 +52,32 @@ At any point in your program, you can start the reactor and a root task using th
52
52
```ruby
53
53
Asyncdo
54
54
3.times do |i|
55
-
sleep1
55
+
sleep(i)
56
56
puts"Hello World #{i}"
57
57
end
58
58
end
59
59
```
60
60
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.
62
62
63
63
By using a nested task, we can ensure that each iteration of the loop creates a new task which runs concurrently.
64
64
65
65
```ruby
66
66
Asyncdo
67
67
3.times do |i|
68
68
Asyncdo
69
-
sleep1
69
+
sleep(i)
70
70
puts"Hello World #{i}"
71
71
end
72
72
end
73
73
end
74
74
```
75
75
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".
77
77
78
78
```mermaid
79
79
graph LR
80
-
EL[Event Loop] --> TT[Initial Task]
80
+
REL[Reactor Event Loop] --> TT[Initial Task]
81
81
82
82
TT --> H0[Hello World 0 Task]
83
83
TT --> H1[Hello World 1 Task]
@@ -185,19 +185,19 @@ barrier.wait
185
185
186
186
When a task completes execution, it will enter the `complete` state (or the `failed` state if it raises an unhandled exception).
187
187
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.
189
189
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.
191
191
192
192
```ruby
193
193
Asyncdo
194
194
tasks =3.times.map do |i|
195
195
Asyncdo
196
-
sleep1
196
+
sleep(i)
197
197
puts"Hello World #{i}"
198
198
end
199
199
end
200
-
200
+
201
201
# Stop all the above tasks:
202
202
tasks.each(&:stop)
203
203
end
@@ -213,7 +213,7 @@ barrier = Async::Barrier.new
213
213
Asyncdo
214
214
tasks =3.times.map do |i|
215
215
barrier.async do
216
-
sleep1
216
+
sleep(i)
217
217
puts"Hello World #{i}"
218
218
end
219
219
end
@@ -222,15 +222,15 @@ Async do
222
222
end
223
223
```
224
224
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:
226
226
227
227
```ruby
228
228
barrier =Async::Barrier.new
229
229
230
230
Asyncdo
231
231
tasks =3.times.map do |i|
232
232
barrier.async do
233
-
sleep1
233
+
sleep(i)
234
234
puts"Hello World #{i}"
235
235
end
236
236
end
@@ -264,7 +264,7 @@ As tasks run synchronously until they yield back to the reactor, you can guarant
264
264
265
265
## Exception Handling
266
266
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.
268
268
269
269
~~~ruby
270
270
require'async'
@@ -307,7 +307,7 @@ require 'async'
307
307
308
308
Asyncdo |task|
309
309
task.with_timeout(1) do
310
-
sleep100
310
+
sleep(100)
311
311
rescueAsync::TimeoutError
312
312
puts"I timed out 99 seconds early!"
313
313
end
@@ -321,11 +321,33 @@ Sometimes you need to do some recurring work in a loop. Often it's best to measu
321
321
~~~ruby
322
322
require'async'
323
323
324
+
period =30
325
+
324
326
Asyncdo |task|
325
327
loopdo
326
328
putsTime.now
327
329
# ... process job ...
328
-
sleep1
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
+
Asyncdo |task|
343
+
run_next =Async::Clock.now
344
+
loopdo
345
+
run_next += period
346
+
putsTime.now
347
+
# ... process job ...
348
+
if (remaining = run_next -Async::Clock.now) >0
349
+
sleep(remaining)
350
+
end
329
351
end
330
352
end
331
353
~~~
@@ -340,9 +362,8 @@ Tasks which are flagged as `transient` do not behave like normal tasks.
340
362
341
363
```ruby
342
364
@pruner=Async(transient:true) do
343
-
loopdo
344
-
345
-
sleep1
365
+
loopdo
366
+
sleep(1)
346
367
prune_connection_pool
347
368
end
348
369
end
@@ -359,43 +380,46 @@ The purpose of transient tasks is when a task is an implementation detail of an
359
380
- A background worker or batch processing job which is independent of any specific operation, and is lazily created.
360
381
- A cache system which needs periodic expiration / revalidation of data/values.
361
382
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`.
363
386
364
387
```ruby
365
388
require'async'
366
389
require'thread/local'# thread-local gem.
367
390
368
-
classTimeCache
391
+
classTimeStringCache
369
392
extendThread::Local# defines `instance` class method that lazy-creates a separate instance per thread
370
393
371
394
definitialize
372
-
@current_time=nil
395
+
@current_time_string=nil
373
396
end
374
397
375
398
defcurrent_time_string
376
399
refresh!
377
400
378
-
return@current_time
401
+
return@current_time_string
379
402
end
380
403
381
404
private
382
405
383
406
defrefresh!
384
407
@refresh||=Async(transient:true) do
385
408
loopdo
386
-
@current_time=Time.now.to_s
409
+
@current_time_string=Time.now.to_s
387
410
sleep(1)
388
411
end
389
412
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:
391
416
@refresh=nil
392
417
end
393
418
end
394
419
end
395
420
396
421
Asyncdo
397
-
# If you are handling 1000s of requests per second, it can be an advantage to cache the current time string.
0 commit comments