# No more threadid indexing? \[thread-local storage\]

**URL:** <https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535>\
**Category:** Performance\
**Tags:** multithreading, threads\
**Created:** [August 11, 2025, 7:08pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535 "2025-08-11T19:08:58Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![ufechner7](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ufechner7/32/51363_2.png) [@ufechner7](https://discourse.julialang.org/u/ufechner7)\
**Post date:** [August 11, 2025, 7:08pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/1 "2025-08-11T19:08:58Z")

</div>

I am implementing multi-threaded scientific software, an so far it works quite well.

But now I am told that thread-local storage is no longer supported by Julia. Well, it still works, even under Julia 1.12rc1, but i am told not to use it.

What is the suggested alternative?

Shell I replace `@threads` with `@spawn` ?

I don’t think I can put `@spawn` in front of a for loop.

So how can I use task local storage in a for loop? Or shall I not use for loops any longer?

Very confused.

---

<div class="post-metadata">

**Author:** ![goerz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/goerz/32/3269_2.png) [@goerz](https://discourse.julialang.org/u/goerz)\
**Post date:** [August 11, 2025, 7:20pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/2 "2025-08-11T19:20:03Z")

</div>

> [@ufechner7](#):
>
> I am told that thread-local storage is no longer supported by Julia

Can you elaborate on that?

---

<div class="post-metadata">

**Author:** ![giordano](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/giordano/32/2166_2.png) [@giordano](https://discourse.julialang.org/u/giordano)\
**Post date:** [August 11, 2025, 7:22pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/3 "2025-08-11T19:22:19Z")

</div>

> [@ufechner7](#):
>
> but i am told not to use it.

This was widely advertised already over 2 years ago: [PSA: Thread-local state is no longer recommended; Common misconceptions about threadid() and nthreads()](https://discourse.julialang.org/t/psa-thread-local-state-is-no-longer-recommended-common-misconceptions-about-threadid-and-nthreads/101274)

> [@ufechner7](#):
>
> So how can I use task local storage in a for loop? Or shall I not use for loops any longer?

Consider using OhMyThreads.jl: [Thread-Safe Storage · OhMyThreads.jl](https://juliafolds2.github.io/OhMyThreads.jl/stable/literate/tls/tls/#TSS)

---

<div class="post-metadata">

**Author:** ![ufechner7](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ufechner7/32/51363_2.png) [@ufechner7](https://discourse.julialang.org/u/ufechner7)\
**Post date:** [August 11, 2025, 7:24pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/4 "2025-08-11T19:24:43Z")

</div>

> [@giordano](#):
>
> Consider using [OhMyThreads.jl](https://juliaregistries.github.io/General/packages/redirect_to_repo/OhMyThreads): [Thread-Safe Storage · OhMyThreads.jl](https://juliafolds2.github.io/OhMyThreads.jl/stable/literate/tls/tls/#TSS)

Well, I tried it and it had a very bad performance.

---

<div class="post-metadata">

**Author:** ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)\
**Post date:** [August 11, 2025, 7:37pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/5 "2025-08-11T19:37:31Z")

</div>

> [@ufechner7](#):
>
> But now I am told that thread-local storage is no longer supported by Julia. […] So how can I use task local storage in a for loop?

You can’t use an array indexed by `threadid()`, but there are other mechanisms for task-local storage.

For example, the `Base` function `task_local_storage()` gives you a task-local `IdDict`. You can also use other patterns, e.g. a `Channel` as described in [Pattern for managing thread local storage? - #2 by tkf](https://discourse.julialang.org/t/pattern-for-managing-thread-local-storage/54023/2).

---

<div class="post-metadata">

**Author:** ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)\
**Post date:** [August 11, 2025, 7:38pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/6 "2025-08-11T19:38:50Z")

</div>

> [@ufechner7](#):
>
> > [@giordano](#):
> >
> > Consider using [OhMyThreads.jl](https://juliaregistries.github.io/General/packages/redirect_to_repo/OhMyThreads): [Thread-Safe Storage · OhMyThreads.jl](https://juliafolds2.github.io/OhMyThreads.jl/stable/literate/tls/tls/#TSS)
> 
> Well, I tried it and it had a very bad performance.

What did you try? There’s a lot of different suggestions in there for different situations.

---

<div class="post-metadata">

**Author:** ![ufechner7](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ufechner7/32/51363_2.png) [@ufechner7](https://discourse.julialang.org/u/ufechner7)\
**Post date:** [August 11, 2025, 7:43pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/7 "2025-08-11T19:43:52Z")

</div>

> [@stevengj](#):
>
> For example, the `Base` function `task_local_storage()` gives you a task-local `IdDict`. You can also use other patterns, e.g. a `Channel` as described in [Pattern for managing thread local storage? - #2 by tkf](https://discourse.julialang.org/t/pattern-for-managing-thread-local-storage/54023/2).

This is too complicated for me. I do not have a degree in computer science, only in electrical engineering. Thread local storage is an easy to understand pattern for engineers and researchers.

---

<div class="post-metadata">

**Author:** ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)\
**Post date:** [August 11, 2025, 7:53pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/8 "2025-08-11T19:53:06Z")

</div>

> [@ufechner7](#):
>
> This is too complicated for me. I do not have a degree in computer science

For example, instead of using a value `mydata[threadid()]`, you could use `task_local_storage(mydata)`, or e.g. `get(task_local_storage(), mydata, default)` if you want a `default` value, where `mydata` is some constant (equal for all tasks), typically a global unique symbol (e.g. `const mydata = gensym(@ __MODULE__ )`). Ideally appending `::T` to tell Julia the type `T` of the returned value (since it is an `Any` dictionary).

---

<div class="post-metadata">

**Author:** ![ufechner7](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ufechner7/32/51363_2.png) [@ufechner7](https://discourse.julialang.org/u/ufechner7)\
**Post date:** [August 11, 2025, 7:56pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/9 "2025-08-11T19:56:25Z")

</div>

I don’t think you can achieve a good performance with dictionaries. Arrays are much faster.

---

<div class="post-metadata">

**Author:** ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)\
**Post date:** [August 11, 2025, 7:59pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/10 "2025-08-11T19:59:17Z")

</div>

> [@ufechner7](#):
>
> I don’t think you can achieve a good performance with dictionaries. Arrays are much faster.

Depends on how performance-critical access to the task-local variable is. What is your application where the cost of a dictionary lookup is significant compared to your other calculations in the task? Maybe in your case there is a better abstraction.

---

<div class="post-metadata">

**Author:** ![ufechner7](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ufechner7/32/51363_2.png) [@ufechner7](https://discourse.julialang.org/u/ufechner7)\
**Post date:** [August 11, 2025, 8:02pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/11 "2025-08-11T20:02:58Z")

</div>

> [@stevengj](#):
>
> Depends on how performance-critical access to the task-local variable is. What is your application where the cost of a dictionary lookup is significant compared to your other calculations in the task?

This is the function we are talking about: [FLORIDyn.jl/src/visualisation/calc\_flowfield.jl at 31081b8e695f5b05f0c3282c365d3eb348f48999 · ufechner7/FLORIDyn.jl · GitHub](https://github.com/ufechner7/FLORIDyn.jl/blob/31081b8e695f5b05f0c3282c365d3eb348f48999/src/visualisation/calc_flowfield.jl#L328C1-L383C1)

---

<div class="post-metadata">

**Author:** ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)\
**Post date:** [August 11, 2025, 8:09pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/12 "2025-08-11T20:09:44Z")

</div>

> [@ufechner7](#):
>
> This is the function we are talking about: [FLORIDyn.jl/src/visualisation/calc\_flowfield.jl at 31081b8e695f5b05f0c3282c365d3eb348f48999 · ufechner7/FLORIDyn.jl · GitHub](https://github.com/ufechner7/FLORIDyn.jl/blob/31081b8e695f5b05f0c3282c365d3eb348f48999/src/visualisation/calc_flowfield.jl#L328C1-L383C1)

In other words, you’d be replacing the line

```julia-auto
GP = buffers.thread_buffers[tid]

```

with something like

```julia-auto
GP = get!(task_local_storage(), :FLORIDyn_buffer) do
   # create new buffer if it doesn't exist yet for this task
end::WindFarm

```

Since this is only executed once per iteration, and the rest of the iteration (the rest of your `for` loop body) seems to do a lot of other work, why would the cost of a dictionary lookup matter?

---

<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:** [August 11, 2025, 8:11pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/13 "2025-08-11T20:11:12Z")

</div>

I think the way to make the least modifications in a code that previously used `threadid` is to use ChunkSplitters.jl, by just replacing the threaded loop by a threaded loop over the chunks of the data:

```julia-repl
julia> using ChunkSplitters, Base.Threads

julia> my_arr = rand(10_000);

julia> nchunks = 10
       my_sum = zeros(10)
       @threads for (ichunk, inds) in enumerate(index_chunks(my_arr; n=nchunks))
           my_sum[ichunk] += sum(@view(my_arr[inds]))
       end
       sum(my_sum)
5033.886812176603

# replacement to
julia> my_sum = zeros(10)
       @threads for i in eachindex(my_arr)
           my_sum[threadid()] += my_arr[i]
       end
       sum(my_sum)
5033.886812176624

```

but OhMyThreads.jl is a higher-level alternative and is probably, most times, a better option after some initial small effort to rewrite the structure of the parallel code.

ps: In your case you would do:

```julia-auto
    buffers = create_thread_buffers(wf, nth)
    # Parallel loop using @threads
    using ChunkSplitters: chunks
    @threads for (tid, iGP_range) in enumerate(chunks(1:length(mx); n=nth))
        # Get thread-local buffers
        GP = buffers.thread_buffers[tid] # tid is now the chunk index
        comp_buffers = buffers.thread_comp_buffers[tid]
        for iGP in iGP_range
            # current calculations using iGP
        end
    end

```

(note that with that `nth` does not be necessarily equal to `nthreads()`, which can be useful to control the number of threads used, if `nth < nthreads()` or increase the number of tasks sometimes improving workload balance, if `nth >> nthreads()`).

---

<div class="post-metadata">

**Author:** ![ufechner7](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ufechner7/32/51363_2.png) [@ufechner7](https://discourse.julialang.org/u/ufechner7)\
**Post date:** [August 11, 2025, 8:25pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/14 "2025-08-11T20:25:52Z")

</div>

All of these solutions look much more complex than what I have now. Is there any good reason to depreciate simple, performant, working solution in favor of complex, confusing solutions? Why are making it more and more difficult to write performant code in Julia?

---

<div class="post-metadata">

**Author:** ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)\
**Post date:** [August 11, 2025, 8:30pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/15 "2025-08-11T20:30:49Z")

</div>

> [@ufechner7](#):
>
> there any good reason to depreciate simple, performant, working solution in favor of complex, confusing solutions?

Yes: allowing tasks to migrate between threads allows for much more flexible and performant parallelism, especially for irregular parallelism where you don’t know in advance how to equally divide the work among threads (i.e. where you need dynamic load balancing).

(In OpenMP, the same thing happens if you use an “untied” task, and [their documentation](https://www.openmp.org/spec-html/5.0/openmpsu113.html) warns against using the thread number in this case.)

(Caveat: I’m not involved in the details of Julia’s task scheduler; task migration is just a general principle of composable parallelism in my understanding: when a thread is idle, it might need to “steal” work from another thread.)

---

<div class="post-metadata">

**Author:** ![ufechner7](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ufechner7/32/51363_2.png) [@ufechner7](https://discourse.julialang.org/u/ufechner7)\
**Post date:** [August 11, 2025, 9:06pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/16 "2025-08-11T21:06:18Z")

</div>

So can I conclude that it is discouraged to use `@threads` in front of a for loop? At least the “good” example at [Multi-Threading · The Julia Language](https://docs.julialang.org/en/v1/manual/multi-threading/#Using-@threads-without-data-races) is not using `@threads` any more but `@spawn`.

On the other hand, you suggested to continue to use `@threads` together with ChunkSplitters .

And the `@threads` macro is not longer creating threads but tasks that can move between threads?

Still very confused.

---

<div class="post-metadata">

**Author:** ![ufechner7](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ufechner7/32/51363_2.png) [@ufechner7](https://discourse.julialang.org/u/ufechner7)\
**Post date:** [August 11, 2025, 9:35pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/17 "2025-08-11T21:35:13Z")

</div>

> [@lmiq](#):
>
> ```julia-auto
> buffers = create_thread_buffers(wf, nth)
> # Parallel loop using @threads
> using ChunkSplitters: chunks
> @threads for (tid, iGP_range) in enumerate(chunks(1:length(mx); n=nth))
> # Get thread-local buffers
> GP = buffers.thread_buffers[tid] # tid is now the chunk index
> comp_buffers = buffers.thread_comp_buffers[tid]
> for iGP in iGP_range
> # current calculations using iGP
> end
> end
> 
> ```

I guess I will try this suggestion and see how it performs.

---

<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:** [August 11, 2025, 10:07pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/18 "2025-08-11T22:07:51Z")

</div>

> [@ufechner7](#):
>
> So can I conclude that it is discouraged to use `@threads` in front of a for loop?

The thing that’s discouraged is using `Threads.threadid()` as an index into preallocated storage. Not just discouraged—you may get incorrect results!

`Threads.@threads` is still a nice and easy solution in many cases, as long as you avoid that pattern.

That said, when your algorithm needs some kind of task local storage, you may actually find it easier to obtain a correct and concise solution using the primitives in OhMyThreads.jl instead.

---

<div class="post-metadata">

**Author:** ![jling](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jling/32/212909_2.png) [@jling](https://discourse.julialang.org/u/jling)\
**Post date:** [August 11, 2025, 11:11pm UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/19 "2025-08-11T23:11:08Z")

</div>

wait, if you have `@threads :static for iGP in 1:length(mx)` , it’s fine right?

---

<div class="post-metadata">

**Author:** ![carstenbauer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/carstenbauer/32/4981_2.png) [@carstenbauer](https://discourse.julialang.org/u/carstenbauer)\
**Post date:** [August 12, 2025, 4:29am UTC](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535/20 "2025-08-12T04:29:43Z")

</div>

> [@ufechner7](#):
>
> So can I conclude that it is discouraged to use `@threads` in front of a for loop? At least the “good” example at [Multi-Threading · The Julia Language](https://docs.julialang.org/en/v1/manual/multi-threading/#Using-@threads-without-data-races) is not using `@threads` any more but `@spawn`.

No, it’s not explicitly discouraged, but using `threadid()` is. Note, though, that `@threads` has always been very primitive and limited API.  
Using it in conjunction with ChunkSplitters.jl makes it a bit more versatile but OhMyThreads.jl tries to provide a better API so that you don’t have to

> [@ufechner7](#):
>
> And the `@threads` macro is not longer creating threads but tasks that can move between threads?

It never created threads. It always created tasks. What has changed is that tasks, created by `@threads`, used to be `sticky` and thus were “pinned” to threads and couldn’t migrate. They now can (since Julia 1.10 IIRC). Hence the 1-1 tasks to threads mapping is generally gone (unless you actively try to restore it).

[Next page](https://discourse.julialang.org/t/no-more-threadid-indexing-thread-local-storage/131535.md?page=2)
