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

According to the release notes:

Tasks can now migrate among threads when they are re-scheduled. Previously, a Task would always run on whichever thread executed it first

Does this mean that the thread I’m running on can no longer be controlled? I was working on an OpenGL project, and OpenGL requires that all calls essentially happen on a single thread, so if I can’t depend on my task staying on the same thread anymore, then it’s virtually impossible to write OpenGL code in Julia.

1 Like

See Thread affinitization: pinning Julia threads to cores - #5 by carstenbauer

That discussion leads to a package, ThreadPinning.jl, which only supports Linux.

Is there really no standard way to do this? That seems like a serious regression; OpenGL isn’t the only un-thread-safe C library…

1 Like

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

5 Likes

Thank you for the detailed explanation!

1 Like

Isn’t it really that you can’t call OpenGL from multiple threads at once? Does it really know which OS thread it’s being called from? I mean, I suppose it might create some kind of thread local storage or something… but usually it’d just mean that you can’t have multiple threads calling it at once. When a task switches from one julia thread to another it’s still a single thread at a time that is calling the OpenGL code.

1 Like

An OpenGL “context” is attached to a specific thread, acting as a thread-local singleton. You can detach and reattach it to another thread, but it’s not a trivial process and certainly not something you want to have to constantly check for.

1 Like