Debugging tasks can be quite challenging. I used to have this prototype project where the tasks, spawned a task which spawned a task. It was a bad design, but for purposes to get it done, I found that redefining the @async macro like:
import Base.sync_varname
import Base.@async
macro async(expr)
tryexpr = quote
try
$expr
catch err
@warn "error within async" exception=err # line $(__source__.line):
@show stacktrace(catch_backtrace())
end
end
thunk = esc(:(()->($tryexpr)))
var = esc(sync_varname)
quote
local task = Task($thunk)
if $(Expr(:isdefined, var))
push!($var, task)
end
schedule(task)
task
end
end
works quite well while debugging.
From my limited experience, and this really is a matter of opinion, a good strategy is also to test the spawned function separately. To do that, it helps to use channels for IO. For instance, reading a string from a socket can be refactored into reading byte string from a Channel, which is easier to test. In addition to avoiding introducing more tasks, one can subtype AbstractChannel, and that way insert a code dealing with the IO object.
Another thing that helps in debugging tasks is reformulating the logic in communicating finite state machines. Something like:
newstate, newmsg = step(state, msg)
works quite well with Julia’s multiple dispatch mechanism. This design also goes hand in hand with the ability to notify condition condition = Threads.Condition() with a value like:
notify(condition, state)
enabling to debug the code within the task without using the @show method.