Interrupts in @async code

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).