Relation of coroutines, threads, and Tasks

A simple version of my question is “How can Tasks and Channels, which are coroutine-related, be used safely with threads?”

The “how” here is more in these sense of “how is it possible” than “how do I code it”, since there are already examples of that. Although it is also in the sense “how do I know which patterns are safe”?

The manual’s initial discussion of parallel computing describes the first approach as involving Tasks, coroutines, and Channels. The second approach uses threads. Even this initial discussion mentions threads being used to run Tasks.

Channels are presented as a way to safely communicate between coroutines. But this answer uses a Channel to communicate between threads.

I would not assume that a mechanism that provided safe communication among coroutines would provide safe communication between OS threads, which have their own set of locking primitives. And I don’t see anything in the documentation of Channel to suggest it is thread-safe.

But the manual clearly indicates that Tasks and coroutines are intended to go together, and a lot of responses in this forum by people clearly more familiar than I with parallelism in julia do mix them. So what’s going on?

In particular, how do I know what forms of mixing are safe and what aren’t?

And how is it that they do mix? For example, architecturally is it that coroutines are used to manage the threads, so that they are really at 2 distinct layers?

My understanding is that coroutines, also known as green threads, are quite distinct from operating system threads. In some ways they are opposites. Green threads involve less overhead than OS threads and so solve the excessive resource demands of the latter. On the other hand, OS threads can preempt each other, while coroutines must explicitly yield control to allow others to run in a process referred to as cooperative multitasking. OS threads can use hardware resources for real parallel execution, while green threads are about letting each other work while they wait for input, i.e., the computer is only handling one at a time. Core.Task is a coroutine according to the manual.

1 Like

If we are just talking solely about Julia and it’s standard library, the only interface you have to threads are Tasks.

Notice that when you use Thread.@spawn you get a Task:

julia> Threads.@spawn sleep(5)
Task (runnable) @0x00007f3fee5beca0

If you look under the hood, you will see that all Theads.@spawn does is create a Task and sets the sticky property to false.

julia> @macroexpand Threads.@spawn sleep(5)
quote
    #= threadingconstructs.jl:181 =#
    let
        #= threadingconstructs.jl:182 =#
        local var"#3#task" = Base.Threads.Task((()->begin
                            #= threadingconstructs.jl:178 =#
                            sleep(5)
                        end))
        #= threadingconstructs.jl:183 =#
        (var"#3#task").sticky = false
        #= threadingconstructs.jl:184 =#
        if $(Expr(:islocal, Symbol("##sync#41")))
            #= threadingconstructs.jl:185 =#
            Base.Threads.put!(var"##sync#41", var"#3#task")
        end
        #= threadingconstructs.jl:187 =#
        Base.Threads.schedule(var"#3#task")
        #= threadingconstructs.jl:188 =#
        var"#3#task"
    end
end

The interface to threads in Julia is Task-based. Sometimes the Task may run on a different thread if you used Threads.@spawn.

A Channel is used for communicating between Tasks whether be on the same thread or not. If you look in the documentation of Channel you should see a spawn keyword:

If spawn = true, the Task created for func may be scheduled on another thread in parallel, equivalent to creating a task via Threads.@spawn.

1 Like

What about Thread.@threads? Do you mean that too uses Tasks underneath? @threads and @threadcall (“may be removed/changed in future versions of Julia”) are the only ones described in Multi-Threading · The Julia Language, though it has a couple of cryptic mentions of @spawn.

Thanks to your pointer to @spawn I got something going using threads and tasks. The @threads didn’t fit because I had per-thread setup I needed to do.

So I guess Task is a coroutine that may sometimes be executed off the main thread.

This means that one of the usual advantages of coroutines, that one can often avoid locking because when and where control is yielded can be controlled, actually does not apply to Julia coroutines when they are running on multiple threads, right?

1 Like

@threadcall for calling external libraries. If that is not what you are doing, I would not use that.

Here is the documentation for Threads.@spawn:

https://docs.julialang.org/en/v1/base/multi-threading/#Base.Threads.@spawn

Whether a Task executes on the current thread or a different thread, it will not block execution on current thread unless you wait for it. @threads for is a construct to simplify launching multiple Tasks on different threads and then it waits for all of them to complete.

Yes, I’m aware. The point I was trying to make is that the introductory discussion of threads provides almost nothing about @spawn (despite 2 references to @spawn that presume we already know what it is), and it seems to me it should. Instead it’s all about @threads and @threadcall. You are correct: @threadcall is not for my current case.