How does Julia Async Task Scheduler work?

I want to understand how the Julia task scheduler works. Like I get how the Python asyncio async mechanism works, but I find myself unable to find anything about that topic in julia.
I know you don’t have to annotate the functions separately. I’ve also tried to figure out if the functions are compiled separately as an async variant, but if I did not make any errors, they are not.
I’ve already tried to get to know the async scheduler the julia GitHub, but that didn’t help me very much either.

So my question is how does the basic task-scheduler mechanism work?

I think async tasks are handled with multithreading. Just spawn a method on a different thread with Threads.@spawn to run the method asynchronously which returns a Task which can be awaited with the fetch method. I.e.:

task = Threads.@spawn my_func()
result = fetch(task)

@spawn will launch a task that will be scheduled to run immediately (on a Julia thread) and return to the calling thread with a reference to that task. If you want to await the result you can use the fetch function. If the task returns nothing, you can use the wait function instead.

If you open Julia with multiple threads (e.g. julia --threads=4) you can run the tasks in parallel, not just asynchronously (as long as you have a multicore CPU).

See Announcing composable multi-threaded parallelism in Julia

Basically, Julia was designed to use multitasking as its fundamental parallelism model based on coroutines in Cilk and Go. Everything is running on a Task. You can obtain the current Task via Base.current_task().

No the functions are not compiled separately. They are just scheduled to run on another task.

If you want to look at the actual implementation, expand the macros.

julia> f() = println("Hello world")
f (generic function with 1 method)

julia> @async f()
Hello world
Task (runnable) @0x0000020097e4ee90

julia> @macroexpand @async f()
quote
    #= task.jl:487 =#
    let
        #= task.jl:488 =#
        local var"#5#task" = Base.Task((()->begin
                            #= task.jl:484 =#
                            f()
                        end))
        #= task.jl:489 =#
        if $(Expr(:islocal, Symbol("##sync#48")))
            #= task.jl:490 =#
            Base.put!(var"##sync#48", var"#5#task")
        end
        #= task.jl:492 =#
        Base.schedule(var"#5#task")
        #= task.jl:493 =#
        var"#5#task"
    end
end
1 Like

It’s also worth noting that there are two different ways to spawn new tasks:

  1. @async which will always run the task on the same hardware thread as the current task, and will only make progress when the current task blocks and yields back to the scheduler;
  2. @spawn which creates a task that can be run on any hardware thread, so it can make progress while the current task is still working if there are other threads available.

You can synchronize both with @sync.

7 Likes