I’m trying to wrap my head around how Julia handles interrupts in @async
code. I have three different scripts that I’m trying to interrupt using ctrl+c
, with various degrees of success.
Script1:
julia> @async begin
for _ = 1:10
println("Hey apple!")
sleep(0.5)
end
end
This cannot be interrupted, instead it prints “Hey apple” ten times regardless of how many times I hit ctrl+c
.
Script 2
julia> @sync begin
@async begin
for _ = 1:10
println("Hey apple!")
sleep(0.5)
end
end
end
This terminates the first time I hit ctrl+c
.
Script 3
julia> @sync begin
@async begin
for _ = 1:10
println("Hey apple!")
sleep(0.5)
end
end
@async begin
for _ = 1:10
println("What?")
sleep(1.0)
end
end
end
Hitting ctrl+c
on this terminates the “Hey apple!” task, but doesn’t stop the “What?” task.
The behaviour of scripts 1 and 2 seems somewhat sensible, but the behaviour of script 3 feels like a bug. Am I missing something?
In this particular case I don’t see any issue related to the handling of the interrupt generated by Ctrl-C
.
The thing to keep in mind is that the InterruptException
is delivered only to one concurrent task, it is not broadcasted to all coroutines.
In Script1 Ctrl-C
delivers the InterruptException
to the REPL interpreter: there is no active computation in the REPL and nothing happens.
The @async task continues until normal termination.
In Script2 Ctrl-C
delivers an InterruptException
to the @sync block and precisely to the only async
task that it wraps: this terminate the @sync block because no other @async tasks are to be waited.
In Script3 Ctrl-C
delivers an InterruptException
to the @sync computation that delivers the exception to the first @async task, but the second @async task continues until normal termination.
In the latter case the @sync block ends when all @async tasks have been completed and if any @async task is terminated by an exception
then a composite exception (a list of all exceptions thrown by errored async tasks) is reported by the sync block when it terminates.
In this case the composite exception will be a list containing a single InterruptException
.
julia> @sync begin
@async begin
for _ = 1:10
println("Hey apple!")
sleep(0.5)
end
end
@async begin
for _ = 1:10
println("What?")
sleep(1.0)
end
end
end
Hey apple!
What?
Hey apple!
What?
Hey apple!
^CWhat?
What?
What?
What?
What?
What?
What?
What?
ERROR: TaskFailedException
nested task error: InterruptException:
Stacktrace:
[1] poptask(W::Base.InvasiveLinkedListSynchronized{Task})
@ Base ./task.jl:921
[2] wait()
@ Base ./task.jl:930
[3] wait(c::Base.GenericCondition{Base.Threads.SpinLock})
@ Base ./condition.jl:124
[4] _trywait(t::Timer)
@ Base ./asyncevent.jl:138
[5] wait
@ ./asyncevent.jl:155 [inlined]
[6] sleep(sec::Float64)
@ Base ./asyncevent.jl:240
[7] macro expansion
@ ./REPL[3]:5 [inlined]
[8] (::var"#3#5")()
@ Main ./task.jl:484
Stacktrace:
[1] sync_end(c::Channel{Any})
@ Base ./task.jl:436
[2] top-level scope
@ task.jl:455
1 Like
That explains the observations, but I still think the behavior of script 3 is somewhat peculiar. I can see that sometimes you may want to have some sort of demon task that shouldn’t be interrupted by normal user interaction, so I’m fine with script 1 behaving the way it does. I can’t imagine a use case where you’d want to interrupt only a single out of many seemingly equivalent tasks, though. I therefore still feel like interrupting an @sync
should interrupt all tasks contained within it (or else not interrupt any task, but then it would be nice if there was another macro for this purpose).