The @sync
macro creates an array Any[]
that is used to keep track of any @async
tasks that are created within the (lexically-scoped) @sync
block. After the code in the @sync
block runs, the Base.sync_end
function is called. This function waits on each task in the aforementioned array so as to make sure that control does not flow forward until after all of the @async
tasks have completed.
You can check out the source code for the @sync
and @async
macros in the the task.jl source file.
The when the sync_end
function iterates over this array of @async
tasks to wait for each one of them, it catches any exceptions raised by the @async
tasks and wraps them in a CompositeException
, which is then thrown to @sync
’s caller. Here is an example of a CompositeException being thrown into outer scope after an asynchronous error within a @sync
block:
julia> @sync begin
t = @async begin
error("asynchronous error")
end
end
ERROR: asynchronous error
error(::String) at ./error.jl:33
macro expansion at ./REPL[1]:3 [inlined]
(::getfield(Main, Symbol("##3#4")))() at ./task.jl:259
Stacktrace:
[1] sync_end(::Array{Any,1}) at ./task.jl:226
[2] top-level scope at task.jl:245
You can see in the stacktrace that sync_end
was involved in catching the error raised by the async task. If multiple asynchronous errors arise, they are all included in the exception that is raised:
julia> @sync begin
t1 = @async begin
error("asynchronous error 1")
end
t2 = @async begin
error("asynchronous error 2")
end
end
ERROR: asynchronous error 1
error(::String) at ./error.jl:33
macro expansion at ./REPL[3]:3 [inlined]
(::getfield(Main, Symbol("##5#7")))() at ./task.jl:259
...and 1 more exception(s).
Stacktrace:
[1] sync_end(::Array{Any,1}) at ./task.jl:226
[2] top-level scope at task.jl:245
Note the ... and 1 more exceptions(s)
message, indicating that the second asynchronous error has not disappeared into the void (rather, it is accessible via the fields of the CompositeException
that was raised).
Of course, synchronous errors are propagated as well:
julia> @sync begin
error("synchronous error")
end
ERROR: synchronous error
Stacktrace:
[1] error(::String) at ./error.jl:33
[2] top-level scope at task.jl:244
In this case sync_end
does not appear in the stacktrace, as control never reached the call to sync_end
at the end of the @sync
call. Rather, control was interrupted by the error
in the middle of executing the sync block.
However, I’ve noticed that, when a synchronous error occurs within a @sync
block, any asynchronous errors that occur are not caught by the call to sync_end
:
julia> @sync begin
t = @async begin
error("asynchronous error")
end
sleep(1)
error("synchronous error")
end
ERROR: synchronous error
Stacktrace:
[1] error(::String) at ./error.jl:33
[2] top-level scope at task.jl:244
The asynchronous error is not caught, and no CompositeException
is raised, as control never reaches the call to sync_end
. The call to sleep(1)
gives the @async
task t
time to execute, so the asynchronous error probably occurs before the synchronous error. But the asynchronous error never propagates to the user.
I feel that something is left to be desiered, because the @sync
block can handle either 1) catching and propagating multiple @async
errors, or 2) catching and propagating a synchronous error, but it is not able to simultaneously handle asynchronous and synchronous errors.
Should this be considered a bug?