@@ -24,15 +24,15 @@ return a vector of results. The input vector will have a length of `batch_size`
24
24
25
25
# Exception handling
26
26
27
- Individual exceptions thrown by `f` will be wrapped in a `TaskFailedException `.
27
+ Individual exceptions thrown by `f` will be wrapped in a `CapturedException `.
28
28
As multiple tasks are used, more than one exception may be thrown. Exceptions
29
29
are combined into a `CompositeException`. Even if only a single exception is
30
30
thrown, it is still wrapped in a `CompositeException`.
31
31
32
- However, when an exception is thrown `asyncmap` will fail-fast, canceling any
33
- remaining work. If you need `asyncmap` to be error resistant then wrap the
34
- body of 'f' in a `try... catch` statement. Below is one possible approach to
35
- error handling:
32
+ However, when an exception is thrown `asyncmap` will fail-fast. Any remaining
33
+ iterations, which are not already in progress, will be cancelled. If you need
34
+ `asyncmap` to be error resistant then wrap the body of 'f' in a `try... catch`
35
+ statement. Below is one possible approach to error handling:
36
36
37
37
```
38
38
julia> result = asyncmap(1:2) do x
@@ -99,9 +99,9 @@ julia> asyncmap(batch_func, 1:5; ntasks=2, batch_size=2)
99
99
```
100
100
101
101
!!! note
102
- Currently, all tasks in Julia are executed in a single OS thread co-operatively. Consequently,
102
+ The tasks created by `asyncmap` are executed in a single OS thread co-operatively. Consequently,
103
103
`asyncmap` is beneficial only when the mapping function involves any I/O - disk, network, remote
104
- worker invocation, etc.
104
+ worker invocation, etc. See [`Threads`](@ref) and [`Distributed`](@ref) for alternatives.
105
105
106
106
"""
107
107
function asyncmap (f, c... ; ntasks= 0 , batch_size= nothing )
@@ -126,8 +126,8 @@ function async_usemap(f, c...; ntasks=0, batch_size=nothing)
126
126
else
127
127
exec_func = (r,args) -> (r. x = f (args... ))
128
128
end
129
- chnl, worker_tasks = setup_chnl_and_tasks (exec_func, ntasks, batch_size)
130
- return wrap_n_exec_twice (chnl, worker_tasks, ntasks, exec_func, c... )
129
+ chnl, err_chnl, worker_tasks = setup_chnl_and_tasks (exec_func, ntasks, batch_size)
130
+ return wrap_n_exec_twice (chnl, err_chnl, worker_tasks, ntasks, exec_func, c... )
131
131
end
132
132
133
133
batch_size_err_str (batch_size) = string (" batch_size must be specified as a positive integer. batch_size=" , batch_size)
@@ -161,7 +161,7 @@ function verify_ntasks(iterable, ntasks)
161
161
return ntasks
162
162
end
163
163
164
- function wrap_n_exec_twice (chnl, worker_tasks, ntasks, exec_func, c... )
164
+ function wrap_n_exec_twice (chnl, err_chnl, worker_tasks, ntasks, exec_func, c... )
165
165
# The driver task, creates a Ref object and writes it and the args tuple to
166
166
# the communication channel for processing by a free worker task.
167
167
push_arg_to_channel = (x... ) -> (r= Ref {Any} (nothing ); put! (chnl,(r,x));r)
@@ -171,41 +171,32 @@ function wrap_n_exec_twice(chnl, worker_tasks, ntasks, exec_func, c...)
171
171
# check number of tasks every time, and start one if required.
172
172
# number_tasks > optimal_number is fine, the other way around is inefficient.
173
173
if length (worker_tasks) < ntasks ()
174
- start_worker_task! (worker_tasks, exec_func, chnl)
174
+ start_worker_task! (worker_tasks, exec_func, chnl, err_chnl )
175
175
end
176
176
push_arg_to_channel (x... )
177
177
end
178
178
else
179
179
map_f = push_arg_to_channel
180
180
end
181
- maptwice (map_f, chnl, worker_tasks, c... )
181
+ maptwice (map_f, chnl, err_chnl, worker_tasks, c... )
182
182
end
183
183
184
- function maptwice (wrapped_f, chnl, worker_tasks, c... )
184
+ function maptwice (wrapped_f, chnl, err_chnl, worker_tasks, c... )
185
185
# first run, returns a collection of Refs
186
- asyncrun_excp = nothing
187
186
local asyncrun
188
187
try
189
188
asyncrun = map (wrapped_f, c... )
190
189
catch ex
191
- if isa (ex,InvalidStateException)
192
- # channel could be closed due to exceptions in the async tasks,
193
- # we propagate those errors, if any, over the `put!` failing
194
- # in asyncrun due to a closed channel.
195
- asyncrun_excp = ex
196
- else
197
- rethrow ()
198
- end
190
+ put! (err_chnl, CapturedException (ex, catch_backtrace ()))
199
191
end
200
192
201
193
# close channel and wait for all worker tasks to finish
202
194
close (chnl)
203
-
204
- # check and throw any exceptions from the worker tasks
205
195
@sync foreach (t -> @sync_add (t), worker_tasks)
206
196
207
- # check if there was a genuine problem with asyncrun
208
- (asyncrun_excp != = nothing ) && throw (asyncrun_excp)
197
+ # check for errors and throw them in chronological order
198
+ close (err_chnl)
199
+ isready (err_chnl) && throw (CompositeException (collect (err_chnl)))
209
200
210
201
if isa (asyncrun, Ref)
211
202
# scalar case
@@ -231,35 +222,36 @@ function setup_chnl_and_tasks(exec_func, ntasks, batch_size=nothing)
231
222
# of an error in any of the worker tasks, the channel is closed. This
232
223
# results in the `put!` in the driver task failing immediately.
233
224
chnl = Channel (0 )
225
+ err_chnl = Channel (nt)
234
226
worker_tasks = []
235
- foreach (_ -> start_worker_task! (worker_tasks, exec_func, chnl, batch_size), 1 : nt)
227
+ foreach (_ -> start_worker_task! (worker_tasks, exec_func, chnl, err_chnl, batch_size), 1 : nt)
236
228
yield ()
237
- return (chnl, worker_tasks)
229
+ return (chnl, err_chnl, worker_tasks)
238
230
end
239
231
240
- function start_worker_task! (worker_tasks, exec_func, chnl, batch_size= nothing )
232
+ start_worker_task (exec_func, chnl) = foreach (exec_data -> exec_func (exec_data... ), chnl)
233
+ start_worker_task (exec_func, chnl, :: Nothing ) = start_worker_task (exec_func, chnl)
234
+ start_worker_task (exec_func, chnl, batch_size) = while isopen (chnl)
235
+ # The mapping function expects an array of input args, as it processes
236
+ # elements in a batch.
237
+ batch_collection= Any[]
238
+ n = 0
239
+ for exec_data in chnl
240
+ push! (batch_collection, exec_data)
241
+ n += 1
242
+ (n == batch_size) && break
243
+ end
244
+ if n > 0
245
+ exec_func (batch_collection)
246
+ end
247
+ end
248
+
249
+ function start_worker_task! (worker_tasks, exec_func, chnl, err_chnl, batch_size= nothing )
241
250
t = @async begin
242
251
try
243
- if isa (batch_size, Number)
244
- while isopen (chnl)
245
- # The mapping function expects an array of input args, as it processes
246
- # elements in a batch.
247
- batch_collection= Any[]
248
- n = 0
249
- for exec_data in chnl
250
- push! (batch_collection, exec_data)
251
- n += 1
252
- (n == batch_size) && break
253
- end
254
- if n > 0
255
- exec_func (batch_collection)
256
- end
257
- end
258
- else
259
- for exec_data in chnl
260
- exec_func (exec_data... )
261
- end
262
- end
252
+ start_worker_task (exec_func, chnl, batch_size)
253
+ catch ex
254
+ put! (err_chnl, CapturedException (ex, catch_backtrace ()))
263
255
finally
264
256
close (chnl)
265
257
end
@@ -320,10 +312,11 @@ end
320
312
321
313
mutable struct AsyncCollectorState
322
314
chnl:: Channel
315
+ err_chnl:: Channel
323
316
worker_tasks:: Array{Task,1}
324
317
enum_state # enumerator state
325
- AsyncCollectorState (chnl:: Channel , worker_tasks:: Vector ) =
326
- new (chnl, convert (Vector{Task}, worker_tasks))
318
+ AsyncCollectorState (chnl:: Channel , err_chnl :: Channel , worker_tasks:: Vector ) =
319
+ new (chnl, err_chnl, convert (Vector{Task}, worker_tasks))
327
320
end
328
321
329
322
function iterate (itr:: AsyncCollector )
@@ -343,16 +336,23 @@ function iterate(itr::AsyncCollector)
343
336
else
344
337
exec_func = (i,args) -> (itr. results[i]= itr. f (args... ))
345
338
end
346
- chnl, worker_tasks = setup_chnl_and_tasks ((i,args) -> (itr. results[i]= itr. f (args... )), itr. ntasks, itr. batch_size)
347
- return iterate (itr, AsyncCollectorState (chnl, worker_tasks))
339
+
340
+ chnl, err_chnl, worker_tasks = setup_chnl_and_tasks (itr. ntasks, itr. batch_size) do i, args
341
+ itr. results[i]= itr. f (args... )
342
+ end
343
+
344
+ return iterate (itr, AsyncCollectorState (chnl, err_chnl, worker_tasks))
348
345
end
349
346
350
347
function wait_done (itr:: AsyncCollector , state:: AsyncCollectorState )
351
348
close (state. chnl)
352
349
353
350
# wait for all tasks to finish
354
- foreach (x -> (v = fetch (x); isa (v, Exception) && throw (v) ), state. worker_tasks)
351
+ @sync foreach (t -> @sync_add (t ), state. worker_tasks)
355
352
empty! (state. worker_tasks)
353
+
354
+ close (state. err_chnl)
355
+ isready (state. err_chnl) && throw (CompositeException (collect (state. err_chnl)))
356
356
end
357
357
358
358
function iterate (itr:: AsyncCollector , state:: AsyncCollectorState )
@@ -369,7 +369,14 @@ function iterate(itr::AsyncCollector, state::AsyncCollectorState)
369
369
return nothing
370
370
end
371
371
(i, args), state. enum_state = y
372
- put! (state. chnl, (i, args))
372
+
373
+ try
374
+ put! (state. chnl, (i, args))
375
+ catch ex
376
+ put! (state. err_chnl, ex)
377
+ wait_done (itr, state)
378
+ rethrow () # Should never reach here
379
+ end
373
380
374
381
return (nothing , state)
375
382
end
0 commit comments