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
+34-30Lines changed: 34 additions & 30 deletions
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@ 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 stopping a parent task, it will also stop all it's children tasks. The 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 event loop generally has one root task.
8
8
9
9
```mermaid
10
10
graph LR
@@ -39,15 +39,15 @@ stateDiagram-v2
39
39
stopped --> [*]
40
40
```
41
41
42
-
Tasks are created in the initialized state, and are run by the event loop. During the execution, a task can either complete successfully, fail 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 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}.
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
45
2. In the case the task failed with an unhandled 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
49
49
50
-
At any point in your program, you can start a task using the {ruby Kernel::Async} method:
50
+
At any point in your program, you can start the reactor and a root task using the {ruby Kernel::Async} method:
51
51
52
52
```ruby
53
53
Asyncdo
@@ -100,7 +100,7 @@ end
100
100
101
101
### Performance Considerations
102
102
103
-
Task creation and execution has been heavily optimised. Do not trade program complexity to avoid creating tasks, the cost will almost never be useful.
103
+
Task creation and execution has been heavily optimised. Do not trade program complexity to avoid creating tasks; the cost will almost always exceed the gain.
104
104
105
105
Do consider using correct concurrency primatives like {ruby Async::Semaphore}, {ruby Async::Barrier}, etc, to ensure your program is well-behaved in the presence of large inputs (i.e. don't create an unbounded number of tasks).
106
106
@@ -124,7 +124,7 @@ end
124
124
125
125
## Waiting for Tasks
126
126
127
-
Waiting for a single task is trivial, simply invoke {ruby Async::Task#wait}. To wait for multiple tasks, you may want to use a {ruby Async::Barrier}. You can use {ruby Async::Barrier#async} to create multiple child tasks, and wait for them all to complete using {ruby Async::Barrier#wait}.
127
+
Waiting for a single task is trivial: simply invoke {ruby Async::Task#wait}. To wait for multiple tasks, you may either {ruby Async::Task#wait} on each in turn, or you may want to use a {ruby Async::Barrier}. You can use {ruby Async::Barrier#async} to create multiple child tasks, and wait for them all to complete using {ruby Async::Barrier#wait}.
128
128
129
129
```ruby
130
130
barrier =Async::Barrier.new
@@ -183,9 +183,9 @@ barrier.wait
183
183
184
184
## Stopping a Task
185
185
186
-
When a task completes execution, it will enter
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}). 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, 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.
189
189
190
190
Using the above program as an example, we can
191
191
@@ -205,7 +205,7 @@ end
205
205
206
206
### Stopping all Tasks held in a Barrier
207
207
208
-
To stop (terminate/cancel) the all tasks held in a barrier:
208
+
To stop (terminate/cancel) all the tasks held in a barrier:
209
209
210
210
```ruby
211
211
barrier =Async::Barrier.new
@@ -222,7 +222,7 @@ Async do
222
222
end
223
223
```
224
224
225
-
If you're letting individual tasks held by a barrier
8000
throw unhandled exceptions, be sure to call ({ruby Async::Barrier#stop}):
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:
226
226
227
227
```ruby
228
228
barrier =Async::Barrier.new
@@ -300,57 +300,59 @@ end
300
300
301
301
## Timeouts
302
302
303
-
You can wrap asynchronous operations in a timeout. This ensures that malicious services don't cause your code to block indefinitely.
303
+
You can wrap asynchronous operations in a timeout. This allows you to put an upper bound on how long the enclosed code will run vs. potentially blocking indefinitely. If the enclosed code hasn't completed by the timeout, it will be interrupted with an {ruby Async::TimeoutError} exception.
304
304
305
305
~~~ruby
306
306
require'async'
307
307
308
308
Asyncdo |task|
309
309
task.with_timeout(1) do
310
-
task.sleep100
310
+
sleep100
311
311
rescueAsync::TimeoutError
312
-
puts"I timed out!"
312
+
puts"I timed out 99 seconds early!"
313
313
end
314
314
end
315
315
~~~
316
316
317
-
### Reoccurring Timers
317
+
### Periodic Timers
318
318
319
-
Sometimes you need to do some periodic work in a loop.
319
+
Sometimes you need to do some recurring work in a loop. Often it's best to measure the periodic delay end-to-start, so that your process always takes a break between iterations and doesn't risk spending 100% of its time on the periodic work. In this case, simply call {ruby sleep} between iterations:
320
320
321
321
~~~ruby
322
322
require'async'
323
323
324
324
Asyncdo |task|
325
-
whiletrue
325
+
loopdo
326
326
putsTime.now
327
-
task.sleep1
327
+
# ... process job ...
328
+
sleep1
328
329
end
329
330
end
330
331
~~~
331
332
332
333
## Reactor Lifecycle
333
334
334
-
Generally, the event loop will not exit until all tasks complete. This is informed by {ruby Async::Task#finished?} which checks if the current node has completed execution, which also includes all children. However, there is one exception to this rule: tasks flagged as being transient ({ruby Async::Node#transient?}).
335
+
Generally, the outer reactor will not exit until all tasks complete. This is informed by {ruby Async::Task#finished?} which checks if the current node has completed execution, which also includes all children. However, there is one exception to this rule: tasks flagged as being `transient` ({ruby Async::Node#transient?}).
335
336
336
337
### Transient Tasks
337
338
338
-
Tasks which are flagged as transient, do not behave like normal tasks.
339
+
Tasks which are flagged as `transient` do not behave like normal tasks.
339
340
340
341
```ruby
341
342
@pruner=Async(transient:true) do
342
-
whiletrue
343
+
loopdo
344
+
343
345
sleep1
344
346
prune_connection_pool
345
347
end
346
348
end
347
349
```
348
350
349
-
1. They will not keep the reactor alive, and instead are stopped (with a {ruby Async::Stop} exception) when all other (non-transient) tasks are finished.
350
-
2.If the parent task is finished, any transient tasks will become children of the parent's parent, i.e. they don't keep sub-trees alive.
351
-
3.If you stop a task which has transient children, those transient children will not be stopped and will instead move up the tree.
351
+
1. They are not considered by {ruby Async::Task#finished?}, so they will not keep the reactor alive. Instead, they are stopped (with a {ruby Async::Stop} exception) when all other (non-transient) tasks are finished.
352
+
2.As soon as a parent task is finished, any transient child tasks will be moved up to be children of the parent's parent. This ensures that they never keep a sub-tree alive.
353
+
3.Similarly, if you `stop` a task, any transient child tasks will be moved up the tree as above rather than being stopped.
352
354
353
-
The purpose of transient tasks is when a task is an implementation detail of an object or instance, rather than a concurrency process. Some examples of trasient tasks
355
+
The purpose of transient tasks is when a task is an implementation detail of an object or instance, rather than a concurrency process. Some examples of transient tasks:
354
356
355
357
- A task which is reading or writing data on behalf of a stateful connection object, e.g. HTTP/2 frame reader, Redis cache invalidation, etc.
356
358
- A task which is monitoring and maintaining a connection pool, pruning unused connections or possibly ensuring those connections are periodically checked for activity (ping/pong, etc).
@@ -364,21 +366,23 @@ require 'async'
364
366
require'thread/local'# thread-local gem.
365
367
366
368
classTimeCache
367
-
extendThread::Local
369
+
extendThread::Local# defines `instance` class method that lazy-creates a separate instance per thread
368
370
369
371
definitialize
370
372
@current_time=nil
371
373
end
372
374
373
-
defcurrent_time
375
+
defcurrent_time_string
374
376
refresh!
375
377
376
378
return@current_time
377
379
end
378
380
379
-
privatedefrefresh!
381
+
private
382
+
383
+
defrefresh!
380
384
@refresh||=Async(transient:true) do
381
-
whiletrue
385
+
loopdo
382
386
@current_time=Time.now.to_s
383
387
sleep(1)
384
388
end
@@ -390,9 +394,9 @@ class TimeCache
390
394
end
391
395
392
396
Asyncdo
393
-
# If you are handling 1000s of requests per second, it can be an advantage to cache the current time as a string.
394
-
pTimeCache.instance.current_time
397
+
# If you are handling 1000s of requests per second, it can be an advantage to cache the current time string.
398
+
pTimeCache.instance.current_time_string
395
399
end
396
400
```
397
401
398
-
Upon existing the top level async block, the {ruby @refresh} task will be set to `nil`. Bear in mind, you should not share these resources across threads, doing so would need further locking/coordination.
402
+
Upon existing the top level async block, the {ruby @refresh} task will be set to `nil`. Bear in mind, you should not share these resources across threads; doing so would need some form of mutual exclusion.
0 commit comments