@@ -2,10 +2,10 @@ defmodule Task do
2
2
@ moduledoc """
3
3
Conveniences for spawning and awaiting for tasks.
4
4
5
- Tasks are processes meant to execute one particular
6
- action throughout their life-cycle, often with little or no
7
- communication with other processes. The most common use case
8
- for tasks is to compute a value asynchronously:
5
+ Tasks are processes that meant to execute one particular
6
+ action throughout their life-cycle, often with little
7
+ explicit communication with other processes. The most common
8
+ use case for tasks is to compute a value asynchronously:
9
9
10
10
task = Task.async(fn -> do_some_work() end)
11
11
res = do_some_other_work()
@@ -16,21 +16,28 @@ defmodule Task do
16
16
They are implemented by spawning a process that sends a message
17
17
to the caller once the given computation is performed.
18
18
19
- Besides `async/1` and `await/1`, tasks can also be used be
20
- started as part of supervision trees and dynamically spawned
21
- in remote nodes. We will explore all three scenarios next.
19
+ Besides `async/1` and `await/1`, tasks can also be used as part
20
+ of supervision trees and dynamically spawned in remote nodes.
21
+ We will explore all three scenarios next.
22
22
23
23
## async and await
24
24
25
25
The most common way to spawn a task is with `Task.async/1`. A new
26
- process will be created and linked to the caller. Once the task
27
- action finishes, a message will be sent to the caller with its
28
- result.
26
+ process will be created and this process is linked and monitored
27
+ by the caller. However, the processes are unlinked right before
28
+ the task finishes, allowing the proper error to be triggered only
29
+ on `await/1`.
29
30
30
- `Task.await/1` is used to read the message sent by the task. On
31
- await, Elixir will also setup a monitor to verify if the process
32
- exited with any abnormal reason (or in case exits are being
33
- trapped by the caller).
31
+ This implies three things:
32
+
33
+ 1) In case the caller crashes, the task will be killed and its
34
+ computation will abort;
35
+
36
+ 2) In case the task crashes due to an error, the parent will
37
+ crash only on `await/1`;
38
+
39
+ 3) In case the task crashes because a linked process caused
40
+ it to crash, the parent will crash immediately;
34
41
35
42
## Supervised tasks
36
43
@@ -48,9 +55,14 @@ defmodule Task do
48
55
]
49
56
50
57
Since such tasks are supervised and not directly linked to
51
- the caller, they cannot be awaited on. Note `start_link/1`,
52
- differently from `async/1`, returns `{:ok, pid}` (which is
53
- the result expected by supervision trees).
58
+ the caller, they cannot be awaited on. For such reason,
59
+ differently from `async/1`, `start_link/1` returns `{:ok, pid}`
60
+ (which is the result expected by supervision trees).
61
+
62
+ Such tasks are useful as workers that run during your application
63
+ life-cycle and rarely communicate with other workers. For example,
64
+ a worker that pushes data to another server or a worker that consumes
65
+ events from an event manager and writes it to a log file.
54
66
55
67
## Supervision trees
56
68
@@ -66,7 +78,7 @@ defmodule Task do
66
78
# In the remote node
67
79
Task.Supervisor.start_link(name: :tasks_sup)
68
80
69
- # In the client
81
+ # On the client
70
82
Task.Supervisor.async({:tasks_sup, :remote@local}, fn -> do_work() end)
71
83
72
84
`Task.Supervisor` is more often started in your supervision tree as:
@@ -106,7 +118,7 @@ defmodule Task do
106
118
"""
107
119
@ spec start_link ( module , atom , [ term ] ) :: { :ok , pid }
108
120
def start_link ( mod , fun , args ) do
109
- Task.Supervised . start_link ( { mod , fun , args } )
121
+ Task.Supervised . start_link ( :undefined , { mod , fun , args } )
110
122
end
111
123
112
124
@ doc """
@@ -135,8 +147,9 @@ defmodule Task do
135
147
@ spec async ( module , atom , [ term ] ) :: t
136
148
def async ( mod , fun , args ) do
137
149
mfa = { mod , fun , args }
138
- ref = make_ref
139
- pid = :proc_lib . spawn_link ( Task.Supervised , :async , [ mfa , self ( ) , ref ] )
150
+ pid = :proc_lib . spawn_link ( Task.Supervised , :async , [ self ( ) , mfa ] )
151
+ ref = Process . monitor ( pid )
152
+ send ( pid , { self ( ) , ref } )
140
153
% Task { pid: pid , ref: ref }
141
154
end
142
155
@@ -148,24 +161,67 @@ defmodule Task do
148
161
exit with the same reason as the task.
149
162
"""
150
163
@ spec await ( t , timeout ) :: term | no_return
151
- def await ( % Task { pid: pid , ref: ref } = task , timeout \\ 5000 ) do
152
- mon_ref = Process . monitor ( pid )
153
-
164
+ def await ( % Task { ref: ref } = task , timeout \\ 5000 ) do
154
165
receive do
155
166
{ ^ ref , reply } ->
156
- Process . demonitor ( mon_ref , [ :flush ] )
167
+ Process . demonitor ( ref , [ :flush ] )
157
168
reply
158
- { :DOWN , ^ mon_ref , _ , _ , :noconnection } ->
159
- exit ( { { :nodedown , get_node ( task . pid ) } , { __MODULE__ , :await , [ task , timeout ] } } )
160
- { :DOWN , ^ mon_ref , _ , _ , reason } ->
169
+ { :DOWN , ^ ref , _ , _ , :noconnection } ->
170
+ mfa = { __MODULE__ , :await , [ task , timeout ] }
171
+ exit ( { { :nodedown , get_node ( task . pid ) } , mfa } )
172
+ { :DOWN , ^ ref , _ , _ , reason } ->
161
173
exit ( { reason , { __MODULE__ , :await , [ task , timeout ] } } )
162
174
after
163
175
timeout ->
164
- Process . demonitor ( mon_ref , [ :flush ] )
176
+ Process . demonitor ( ref , [ :flush ] )
165
177
exit ( { :timeout , { __MODULE__ , :await , [ task , timeout ] } } )
166
178
end
167
179
end
168
180
181
+ @ doc """
182
+ Receives a group of tasks and a message and finds
183
+ a task that matches the given message.
184
+
185
+ This function returns a tuple with the task and the
186
+ returned value in case the message matches a task that
187
+ exited with success, it raises in case the found task
188
+ failed or nil if no task was found.
189
+
190
+ This function is useful in situations where multiple
191
+ tasks are spawned and their results are collected just
192
+ later on. For example, a GenServer can spawn tasks,
193
+ store the tasks in a list and later use `Task.find/2`
194
+ to see if upcoming messages are from any of the tasks.
195
+ """
196
+ @ spec find ( [ t ] , any ) :: { term , t } | nil | no_return
197
+ def find ( tasks , msg )
198
+
199
+ def find ( tasks , { ref , reply } ) when is_reference ( ref ) do
200
+ Enum . find_value tasks , fn
201
+ % Task { ref: task_ref } = t when ref == task_ref ->
202
+ Process . demonitor ( ref , [ :flush ] )
203
+ { reply , t }
204
+ % Task { } ->
205
+ nil
206
+ end
207
+ end
208
+
209
+ def find ( tasks , { :DOWN , ref , _ , _ , reason } = msg ) when is_reference ( ref ) do
210
+ find = fn ( % Task { ref: task_ref } ) -> task_ref == ref end
211
+ case Enum . find ( tasks , find ) do
212
+ % Task { pid: pid } when reason == :noconnection ->
213
+ exit ( { { :nodedown , get_node ( pid ) } , { __MODULE__ , :find , [ tasks , msg ] } } )
214
+ % Task { } ->
215
+ exit ( { reason , { __MODULE__ , :find , [ tasks , msg ] } } )
216
+ nil ->
217
+ nil
218
+ end
219
+ end
220
+
221
+ def find ( _tasks , _msg ) do
222
+ nil
223
+ end
224
+
169
225
defp get_node ( { _ , n } ) when is_atom ( n ) , do: n
170
- defp get_node ( pid ) when is_pid ( pid ) , do: pid
226
+ defp get_node ( pid ) when is_pid ( pid ) , do: pid
171
227
end
0 commit comments