# Sharp edge with \`Threads.threadid()\` and task migration

**URL:** https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550
**Category:** General Usage
**Tags:** multithreading
**Created:** [January 8, 2025, 5:32pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550 "2025-01-08T17:32:21Z")
**Posts on this page:** 20
**Page:** 1

<div class="post-metadata">

### Author: ![GeorgeGkountouras](https://avatars.discourse-cdn.com/v4/letter/g/77aa72/32.png) [@GeorgeGkountouras](https://discourse.julialang.org/u/GeorgeGkountouras)
#### Post date: [January 8, 2025, 5:32pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/1 "2025-01-08T17:32:21Z")

</div>

> [@Package for Rust-like borrow checker in Julia?](https://discourse.julialang.org/t/package-for-rust-like-borrow-checker-in-julia/124442/55):
>
> Edit: will just manually check `.threadid == Threads.threadid()` each time anything is accessed.

Your code might get rescheduled on a different thread between the check and the access.

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [January 8, 2025, 7:31pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/2 "2025-01-08T19:31:31Z")

</div>

Edit: note that this was moved from another thread. Which hopefully explains why it feels a bit out-of-place as a first reply on this thread 😓

* * *

> [@GeorgeGkountouras](#):
>
> Your code might get rescheduled on a different thread between the check and the access.

Wait, what!? That’s unexpected. Why doesn’t `Threads.threadid()` just abstract to the task ID instead? It feels strange that a broadly-understood system call like “threadid” would expose arbitrary internal details, especially when tasks are Julia’s main abstraction for parallelism.

It makes me wonder what the intended use case for `threadid()` actually is—because in practice, this seems more likely to cause confusion than provide value. (Sorry for the rant—just trying to work through my thoughts here!)

> **[PSA: Thread-local state is no longer recommended](https://julialang.org/blog/2023/07/PSA-dont-use-threadid/)**
>
> PSA: Thread-local state is no longer recommended; Common misconceptions about threadid() and nthreads()

> This pattern has even been erroneously recommended in [previous official julia blogposts](https://julialang.org/blog/2019/07/multithreading/#thread-local_state).

Ugh! Well, at least I know I’m not misremembering learning this (and then using that officially recommended pattern in all my code).

Why not just fix `threadid()` to align with how people are actually using it?

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [January 8, 2025, 7:41pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/3 "2025-01-08T19:41:06Z")

</div>

Reading through a code search for [`language:julia /threadid/`](https://github.com/search?q=/threadid/+language:julia&type=code&p=1) is pretty terrifying. So much concurrency-based corruption, everywhere 🙈 That is a really really sharp edge. Is it possible for us to patch that in Julia to match the usage == task ID? (I’m happy to write a PR for this)

---

<div class="post-metadata">

### Author: ![mbauman](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mbauman/32/31082_2.png) [@mbauman](https://discourse.julialang.org/u/mbauman)
#### Post date: [January 8, 2025, 7:57pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/4 "2025-01-08T19:57:06Z")

</div>

> [@MilesCranmer](#):
>
> Is it possible for us to patch that in Julia to match the usage == task ID?

I don’t think this would fix the situation in the vast majority of cases — the most common flavor of that anti-pattern is to do:

```julia
tls = Array{Any}(undef, Threads.nthreads())
# ...
tls[Threads.threadid()] = # ...

```

The number of threads used to be bounded (and sometimes still is), but the number of tasks is definitely not. Perhaps in some ways two bugs would be better than one (especially given that the subsequent bounds error would be much noisier), but it would also be lying about its own name.

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [January 8, 2025, 8:01pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/5 "2025-01-08T20:01:46Z")

</div>

Yes I think having `threadid()` match the ID of the task would be _infinitely_ better. Better a BoundsError you can see than a cache corruption you cannot.

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [January 8, 2025, 8:17pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/6 "2025-01-08T20:17:48Z")

</div>

Perhaps an even better solution would be to remove `threadid()` altogether, with an error message that points users to [that blogpost](https://julialang.org/blog/2023/07/PSA-dont-use-threadid/). I see few cases where someone uses it in a way that actually matches what they mean. Removing it with an informative error message would be even better than a BoundsError (which itself would still be infinitely better than silent cache corruption). If even myself, a highly active community member, didn’t realise this behavior of `threadid()` (which in most other languages is constant in a scope), I feel like 99% of the general userbase wouldn’t know.

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [January 8, 2025, 8:29pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/7 "2025-01-08T20:29:24Z")

</div>

> [@MilesCranmer](#):
>
> Perhaps an even better solution would be to remove `threadid()` altogether,

This also looks like this the solution by GoLang, whose concurrency model is very similar to Julia: [their FAQ](https://go.dev/doc/faq#no_goroutine_id):

> [@](#):
>
> Goroutines do not have names; they are just anonymous workers. They expose no unique identifier, name, or data structure to the programmer. Some people are surprised by this, expecting the `go` statement to return some item that can be used to access and control the goroutine later.
> 
> The fundamental reason goroutines are anonymous is so that the full Go language is available when programming concurrent code. By contrast, the usage patterns that develop when threads and goroutines are named can restrict what a library using them can do.

I think it would be really smart to formally deprecate `Threads.threadid()`. Just seems super easy to abuse and generate difficult-to-debug caching issues. Especially if `threadid()` is no longer constant, I don’t see a reason to keep it around other than to print a message teaching the user how to upgrade.

I’m sure many multithreaded libraries written before Julia 1.8 have some sort of silent concurrency issue due to abuse of `threadid`. Better to make those errors loud so they can get fixed!

---

<div class="post-metadata">

### Author: ![Janis\_Erdmanis](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/janis_erdmanis/32/10869_2.png) [@Janis\_Erdmanis](https://discourse.julialang.org/u/Janis_Erdmanis)
#### Post date: [January 8, 2025, 9:05pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/8 "2025-01-08T21:05:27Z")

</div>

The blog post does not address a situation for wraping an arithmetic library where scratch space is needed for operations. Acquiring a lock can incur a significant overhead for fast operations like to compare whether two numbers are equal.

For example, I don’t see how I could have made OpenSSLGroup.jl multithreaded without relying on a [get\_ctx](https://github.com/PeaceFounder/OpenSSLGroups.jl/blob/d34b965db0ca2bb3d6ae3550429d0ae6b25d7f7f/src/OpenSSLGroups.jl#L26) function that provides a context object based on each thread. It is for instance even needed for comparing two points [as can be seen here](https://github.com/PeaceFounder/OpenSSLGroups.jl/blob/d34b965db0ca2bb3d6ae3550429d0ae6b25d7f7f/src/point.jl#L14). Any task switching or locking would add overhead.

I know that what I currently have is not very safe, but I don’t see I could made it without relying on `Threads.threadid()` to maintain performance. I wish that there were a macro that could be placed in front of a function body that ensures that function does not yield or error if such effect could not be proven.

---

<div class="post-metadata">

### Author: ![mbauman](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mbauman/32/31082_2.png) [@mbauman](https://discourse.julialang.org/u/mbauman)
#### Post date: [January 8, 2025, 9:05pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/9 "2025-01-08T21:05:28Z")

</div>

If you’re interested in some archeology, an early version of the patch that allows adding threads [initially removed `nthreads`](https://github.com/JuliaLang/julia/pull/45447#discussion_r934779720) and a warning was also tried in julia#48589. These functions — both `nthreads` and `threadid` — do still do what they say on the tin and there are programs that use them correctly.

Change is hard and there are many tradeoffs. I think the fastest and easiest way to help socialize this is to add a lint warning akin to 1-based indexing. It’s a similar sort of problem — this code might be just fine, but folks are often using it in ways that don’t take into account the entire universe of possible behaviors.

![image](https://global.discourse-cdn.com/julialang/original/3X/5/a/5adbf8a00777a78578b784ce4ecc00896cd18ad6.png)

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [January 8, 2025, 9:22pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/10 "2025-01-08T21:22:03Z")

</div>

I appreciate the suggestion about linting, but I’m concerned about error visibility in legacy code. A linter warning won’t catch runtime issues in existing codebases that are silently corrupting their cache. At least with 1:length(A), you get a clear BoundsError that points you to the problem.

Plus, while 1:length(x) has legitimate uses in code that knows its array types, silent task migration under threadid() is almost never what you want. If there is a need to support these specific deprecated patterns, we should create a new, clearly-named function for it - not silently change the behavior of a previously-recommended API. That’s just asking for untraceable bugs in production.

And unlike array indexing where we have `Base.require_one_based_indexing()`, there’s no way to verify that none of my dependencies are using threadid() incorrectly and potentially generating corrupt results based on code written before 1.8.

---

<div class="post-metadata">

### Author: ![danielwe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danielwe/32/35657_2.png) [@danielwe](https://discourse.julialang.org/u/danielwe)
#### Post date: [January 8, 2025, 10:22pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/11 "2025-01-08T22:22:21Z")

</div>

> [@Janis\_Erdmanis](#):
>
> For example, I don’t see how I could have made [OpenSSLGroup.jl](https://juliaregistries.github.io/General/packages/redirect_to_repo/OpenSSLGroup) multithreaded without relying on a [get\_ctx](https://github.com/PeaceFounder/OpenSSLGroups.jl/blob/d34b965db0ca2bb3d6ae3550429d0ae6b25d7f7f/src/OpenSSLGroups.jl#L26) function that provides a context object based on each thread.

Could you store the context in thread-local storage instead? [Tasks · The Julia Language](https://docs.julialang.org/en/v1/base/parallel/#Base.task_local_storage-Tuple%7BAny%7D)

It could look something like this:

```julia
function get_ctx()
    return get!(OpenSSLContext, task_local_storage(), :ctx)::OpenSSLContext
end

```

This would initialize a new context for every new task rather than once per thread for the lifetime of the program, so there’s a little extra overhead, similar to calling your ` __init__ ` function right before every `@threads` loop. Is that prohibitive for you or not?

Another alternative would be to initialize `nthreads()` contexts like you currently do, but store them in a `Channel` that tasks can take from and put back to. This only has to happen once for the lifetime of each task, i.e., you don’t have to lock for every operation. Perhaps something like this would work:

```julia
function get_ctx()
    return get!(task_local_storage(), :ctx) do
        t = current_task()
        ctx = take!(CONTEXT_CHANNEL)
        Threads.@spawn begin
            # put ctx back once the task using it is finished
            wait(t)
            put!(CONTEXT_CHANNEL, ctx)
        end
        ctx
    end::OpenSSLContext
end

```

Which of these has least overhead depends on how costly it is to allocate a new context object compared to interacting with a `Channel`.

---

<div class="post-metadata">

### Author: ![GunnarFarneback](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gunnarfarneback/32/1827_2.png) [@GunnarFarneback](https://discourse.julialang.org/u/GunnarFarneback)
#### Post date: [January 8, 2025, 10:46pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/12 "2025-01-08T22:46:10Z")

</div>

> [@Janis\_Erdmanis](#):
>
> The blog post does not address a situation for wraping an arithmetic library where scratch space is needed for operations.

The blog post is rather focused on reduction operations. For scratch space I have used the following pattern:

```julia
Threads.@threads for ...
    scratch_data = get!(() -> Vector{Int}(undef, scratch_size),
                        task_local_storage(),
                        :data)::Vector{Int}
    ...
end

```

---

<div class="post-metadata">

### Author: ![Janis\_Erdmanis](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/janis_erdmanis/32/10869_2.png) [@Janis\_Erdmanis](https://discourse.julialang.org/u/Janis_Erdmanis)
#### Post date: [January 8, 2025, 11:02pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/13 "2025-01-08T23:02:21Z")

</div>

Task local storage looks interesting. I did not know about it as I used that blog post as a reference to see alternatives for `Threads.threadid()`. I will try the `task_local_storage` approach someday with more time to spare and report whether that introduces performance regressions. It would also allow me to eliminate the ` __init__ ` block and compile time hacks I needed to do.

The `CONTEXT_CHANNEL` approach seems would add significant overhead to get context from the channel and schedule a short-lived task to put it back in.

---

<div class="post-metadata">

### Author: ![danielwe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danielwe/32/35657_2.png) [@danielwe](https://discourse.julialang.org/u/danielwe)
#### Post date: [January 8, 2025, 11:26pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/14 "2025-01-08T23:26:59Z")

</div>

> [@Janis\_Erdmanis](#):
>
> significant overhead to get context from the channel and schedule a short-lived task to put it back in.

Note that a `Threads.@threads` loop only spawns approximately `nthreads()` tasks and partitions the work among them. The tasks live as long as the loop and perform approximately `n / nthreads()` iterations each. I agree that the channel-based pattern would be prohibitive if each task only did a single iteration, like in the naive `for i in 1:n; Threads.@spawn begin ...` pattern, but when using `@threads` on a loop that’s big enough to warrant multithreading it seems less clear. Would be interesting to see the benchmark.

---

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [January 8, 2025, 11:45pm UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/15 "2025-01-08T23:45:32Z")

</div>

> [@Janis\_Erdmanis](#):
>
> see alternatives for `Threads.threadid()`

I think the most simple alternative is to divide the workload into `ntasks` chunks and index the chunks. This can be rather easily implemented by hand, but it is what [ChunkSplitters.jl](https://juliafolds2.github.io/ChunkSplitters.jl/stable/multithreading/#@threads-and-enumerate) implements.

The higher lever alternative, OhMyThreads.jl also uses that approach and addresses these issues.

(decoupling the number of tasks from `nthreads()` has the additional advantage of controlling the number of tasks independently of how julia was initialized. That can be used used to reduce the parallelization -very useful to benchmark scaling- or to improve load balancing when the tasks are heterogeneous, using `ntasks >> nthreads`).

---

<div class="post-metadata">

### Author: ![Janis\_Erdmanis](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/janis_erdmanis/32/10869_2.png) [@Janis\_Erdmanis](https://discourse.julialang.org/u/Janis_Erdmanis)
#### Post date: [January 9, 2025, 12:02am UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/16 "2025-01-09T00:02:13Z")

</div>

Splitting workload into chunks would be only possible if I exported array API. That would be quite limiting and also hard to maintain.

I think the `task_local_storage` is the solution for my case. I just need to check whether overhead of retrieving the context does not exceed the time it takes to do the operation itself.

---

<div class="post-metadata">

### Author: ![danielwe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danielwe/32/35657_2.png) [@danielwe](https://discourse.julialang.org/u/danielwe)
#### Post date: [January 9, 2025, 12:28am UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/17 "2025-01-09T00:28:17Z")

</div>

> [@Janis\_Erdmanis](#):
>
> I just need to check whether overhead of retrieving the context does not exceed the time it takes to do the operation itself.

For performance, make sure to type assert as soon as you load, since `task_local_storage` is backed by an untyped `IdDict` and therefore not type stable. That is,

```julia
task_local_storage(:ctx)::OpenSSLContext
# or
task_local_storage()[:ctx]::OpenSSLContext
# or
get!(OpenSSLContext, task_local_storage(), :ctx)::OpenSSLContext

```

* * *

(I edited my post above to use the more elegant `get!(f, collection, key)` form after seeing @GunnarFarneback’s post.)

---

<div class="post-metadata">

### Author: ![GunnarFarneback](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gunnarfarneback/32/1827_2.png) [@GunnarFarneback](https://discourse.julialang.org/u/GunnarFarneback)
#### Post date: [January 9, 2025, 6:46am UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/18 "2025-01-09T06:46:07Z")

</div>

Notice that I use `get!` rather than `get` so the freshly generated scratch spaces are stored in `task_local_storage` and reused.

---

<div class="post-metadata">

### Author: ![danielwe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danielwe/32/35657_2.png) [@danielwe](https://discourse.julialang.org/u/danielwe)
#### Post date: [January 9, 2025, 6:55am UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/19 "2025-01-09T06:55:25Z")

</div>

Oh, I thought that’s what `get` did. Read the docstring too quickly and didn’t notice that distinction. Fixing the above posts. Thanks!

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [January 9, 2025, 10:57am UTC](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550/20 "2025-01-09T10:57:42Z")

</div>

One annoyance I have with `task_local_storage` is that it starts fresh for every single task.

So you can’t use it the same way you could use `cache[Threads.threadid()]` unless you have a long-running task… in which case you’d be better off passing a local variable instead. If you are rapidly `Threads.@spawn`ing many workloads, it would just get reset each time.

Like if you try to store a hash ID in `task_local_storage()`:

```julia
julia> let n=1000
           [Threads.@spawn(
                   get!(() -> rand(UInt64), task_local_storage(), :ctx)
               ) for _ in 1:n] |>
             Base.Fix1(map, fetch) |>
             unique |>
             length
       end

1000

```

Each new `Threads.@spawn` gets its own ID. So in other words I would be recreating the cache every single time I spawn a new task.

Like, it just seems as though `task_local_storage()` is a way to do quantum tunnelling of variables around your codebase (bad). But for the one legitimate usecase, caching, I can’t see an effective use.

(Though for some reason `Threads.@threads` is able to create fewer tasks than the number of iterations, so there it seems like you actually can share the cache across loop iterations. Weird.)

[Next page](https://discourse.julialang.org/t/sharp-edge-with-threads-threadid-and-task-migration/124550.md?page=2)
