Julia 1.7 says it can switch the thread your task is on. How often does that happen, and how can it be disabled?

Let’s not confuse threads with tasks here. Primarily, ThreadPinning.jl is for pinning Julia threads to specific cores. That’s it. Whether tasks are started on and/or stay on a certain Julia thread is an entirely different thing.

First things first: Julia implements task-based multithreading (somewhat similar to Go), so the general idea is that users care about tasks and not so much about threads. You just @spawn whatever you want and let the scheduler put this on a certain thread or, if necessary, let the task migrate to another thread if necessary. In some sense, this is great since it abstracts away some of the more low-level scheduling aspects and also facilitates composable / nested multithreading.
However, sometimes you want / need to control on which Julia thread (and, if pinned to a core in some way, which core) a task runs on. The good news is, this is still possible (see below). However, I must say that my feeling is that it is not really supported since it might undermine the idea of composable task-based multithreading (personally, I think we should support both!).

So, how can we “pin” a task to a certain Julia thread? The keyword here is sticky. If you look at the source code of @threads (in particular this function), you see that the task(s) corresponding to the body of the multithreaded loop will get put on specific threads and, importantly, get the property sticky = true. This means, that the task will stay on this Julia thread and won’t migrate away. So, if you write a loop like

@threads for i in 1:nthreads()
    f(i)
end

you can be sure that f(i) will run on the Julia thread i and will stay there. Contrast this to @spawn which has sticky = false. Hence, tasks spawned in that way can, in principle, migrate between threads.

Now you might say, well that’s great but I sometimes want to “spawn” a single task on a specific thread… While you could use @threads for this,

@threads for i in 1:nthreads()
    if i == thread_that_should_run_the_computation
        f()
    end
end

this is arguably overly complex and also somewhat of a misuse of @threads. Fortunately, we can essentially just define our own hybrid of @threads and @spawn with sticky = true. That’s what @tspawnat is which I’ve added to ThreadPinning.jl yesterday (note that ThreadPools.jl also has a @tspawnat but, counterintuitively, it has sticky = false, see this issue). So, you can now just do

using ThreadPinning
t = @tspawnat 3 f()
[...]
fetch(t)

to run f() on the third Julia thread without any task migration.

(Note that while the thread pinning part of ThreadPinning.jl only supports Linux, @tspawnat can, of course, be used on any system since it is only about making Julia tasks sticky.)

8 Likes