How to interrupt async tasks?

Suppose I have a few tasks synchronized by @sync and @async as show below:

running = true
function task()
    i = 0
    while running
        println(i+=1)
        sleep(1)
    end
end

@sync begin
    @async task()
    @async task()
end

How can I catch an InterruptException and set running=false to gracefully shutdown the tasks? Currently Julia will crash immediately:

β‹Š> ~/code julia async-interrupt.jl                                                                              19:58:321
1
2
2
^C
signal (2): Interrupt
in expression starting at /home/ubuntu/code/async-interrupt.jl:14
epoll_wait at /lib/x86_64-linux-gnu/libc.so.6 (unknown line)
uv__io_poll at /workspace/srcdir/libuv/src/unix/epoll.c:240
uv_run at /workspace/srcdir/libuv/src/unix/core.c:383
jl_task_get_next at /buildworker/worker/package_linux64/build/src/partr.c:481
poptask at ./task.jl:827
wait at ./task.jl:836
wait at ./condition.jl:123
_trywait at ./asyncevent.jl:118
wait at ./asyncevent.jl:136 [inlined]
sleep at ./asyncevent.jl:221
task at /home/ubuntu/code/async-interrupt.jl:7
#2 at ./task.jl:423
unknown function (ip: 0x7fd6b2142fdf)
_jl_invoke at /buildworker/worker/package_linux64/build/src/gf.c:2247 [inlined]
jl_apply_generic at /buildworker/worker/package_linux64/build/src/gf.c:2429
jl_apply at /buildworker/worker/package_linux64/build/src/julia.h:1788 [inlined]
start_task at /buildworker/worker/package_linux64/build/src/task.c:877
unknown function (ip: (nil))
Allocations: 2723 (Pool: 2712; Big: 11); GC: 0

Issue #35524 may be related.

You can turn off Ctrl-C

https://docs.julialang.org/en/v1/base/c/#Base.disable_sigint

julia> function job()
    try
        sleep(50)
    catch
        println("woked")
    end
end

julia> try
    disable_sigint() do
        @sync for i in 1:100
            @async job()
        end
    end
catch
    println("ctrl-c")
end

# code is now running
# first ctrl-c
woked
# second ctrl-c
ctrl-c

julia>

I leave doing everything in the right order as an exercise for the reader :slight_smile:

@async returns a Task - you can interact with that, though note that interrupting running tasks from a different tasks is generally not a good idea. It’s usually better to write the tasks in a way that they check some condition from time to time to make sure they can shut down gracefully or have some waitable object that you can close.

EDIT: for some more context:

Thank you! But the problem is that Julia immediately crashes after receiving the interrupt, leaving no chance of executing any code.

prevents such events, I tired my code before I posted it

You can then try ... catch InterruptException ... end to handle that case - though be aware that this is pretty finicky, as it’s not really defined where exactly you’ll get that.

Oops I found the solution. It is because julia exists immediately after receiving a SIGINT by default when running a script. We can disable that by Base.exit_on_sigint(false):

running = true
function task()
    try
        i = 0
        while running
            println(i+=1)
            sleep(1)
        end
        println("Task is stopped")
    catch err
        global running = false
        @error "Task is interrupted" exception = (err, catch_backtrace())
    end
end

Base.exit_on_sigint(false)
@sync begin
    @async task()
    @async task()
end
println("Program exited")

Now the code will exit gracefully after ctrl+c:

 ~/julia ξ‚° ./julia async-interupt.jl
1
1
^Cβ”Œ Error: Task is interrupted
β”‚   exception =
β”‚    InterruptException:
β”‚    Stacktrace:
β”‚     [1] poptask(W::Base.InvasiveLinkedListSynchronized{Task})
β”‚       @ Base ./task.jl:935
β”‚     [2] wait()
β”‚       @ Base ./task.jl:944
β”‚     [3] wait(c::Base.GenericCondition{Base.Threads.SpinLock})
β”‚       @ Base ./condition.jl:124
β”‚     [4] _trywait(t::Timer)
β”‚       @ Base ./asyncevent.jl:129
β”‚     [5] wait
β”‚       @ ./asyncevent.jl:147 [inlined]
β”‚     [6] sleep(sec::Int64)
β”‚       @ Base ./asyncevent.jl:232
β”‚     [7] task()
β”‚       @ Main ~/julia/async-interupt.jl:7
β”‚     [8] (::var"#2#4")()
β”‚       @ Main ./task.jl:490
β”” @ Main ~/julia/async-interupt.jl:11
Task is stopped
Program exited

This is only the behavior if nothing else catches that on the main task. The ambiguity lies in who ultimately receives the SIGINT, which in this case ends up being the main task since you throw a new error in the inner task, which gets rethrown to the @sync task. If you have

try
@sync begin
    @async task()
    @async task()
end
catch err
    # ...
end

It won’t just exit by default, even with exit_on_sigint(true).

1 Like