# Is this pattern still thread safe in Julia 1.12?

**URL:** <https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484>\
**Category:** General Usage\
**Tags:** question, multithreading\
**Created:** [October 28, 2025, 2:47pm UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484 "2025-10-28T14:47:55Z")\
**Posts on this page:** 15\
**Page:** 1

<div class="post-metadata">

**Author:** ![sylvaticus](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/sylvaticus/32/203883_2.png) [@sylvaticus](https://discourse.julialang.org/u/sylvaticus)\
**Post date:** [October 28, 2025, 2:47pm UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/1 "2025-10-28T14:47:55Z")

</div>

I adopt this pattern (1) to allow non default random threads that may be not thread safe themselves and (2) to obtain the same result whatever the number of threads used:

```julia-auto
import StableRNGs, Random

function goo(outseed)
    rng = StableRNGs.StableRNG(outseed) # in the real app rng is choosen by the user
    n_trees = 4
    masterSeed =rand(rng,100:Int(floor(typemax(Int64)/10000))) # Some RNG have problems with very small seeds.
    rngs = [deepcopy(rng) for i in 1:Threads.nthreads()+1]
    results = Vector{Float64}(undef,n_trees)
    Threads.@threads for i in 1:n_trees
        tsrng = rngs[Threads.threadid()] # Thread safe random number generator
        Random.seed!(tsrng,masterSeed+i*10)
        out = rand(tsrng)
        results[i] = out
    end
    return results
end

```

I have noticed that in julia 1.12 I need to specify “Threads.nthreads() **+1** ” (the +1 addition is new), and I am aware that “buffers [should not be managed based on `threadid()`](https://docs.julialang.org/en/v1/manual/multi-threading/#Using-@threads-without-data-races)”.  
However, in my case this is just to host a rng that is re-seeded based on the index of the loop, not on the threadid.

What are your thoughts ?

---

<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:** [October 28, 2025, 4:15pm UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/2 "2025-10-28T16:15:07Z")

</div>

I suspect that’s still problematic . `@threads` aren’t threads, they’re tasks that can be run multi-threaded. You need task-local state, not thread-local state.

Is it truly the RNG itself that is your bottleneck here? Or could you gather all the numbers you need in advance (single-threaded) and then use them multi-threadedly?

---

<div class="post-metadata">

**Author:** ![sylvaticus](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/sylvaticus/32/203883_2.png) [@sylvaticus](https://discourse.julialang.org/u/sylvaticus)\
**Post date:** [October 28, 2025, 4:20pm UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/3 "2025-10-28T16:20:36Z")

</div>

Thanks @mbauman  
My issue with prefetch the random values is that it is not easy to compute the numbers I need in advance, at least not always. Also, I have often inner functions that expect a `rng`.  
I will review this…

---

<div class="post-metadata">

**Author:** ![abraemer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/abraemer/32/51403_2.png) [@abraemer](https://discourse.julialang.org/u/abraemer)\
**Post date:** [October 28, 2025, 8:45pm UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/4 "2025-10-28T20:45:53Z")

</div>

> [@sylvaticus](#):
>
> What are your thoughts ?

If the calculation in the loop is sufficiently long (more than 10-100ms or so), I probably would not bother too much and prefer the simple version where you create a new RNG for each iteration:

```julia-auto
function goo(outseed)
    rng = StableRNGs.StableRNG(outseed) # in the real app rng is choosen by the user
    n_trees = 4
    masterSeed =rand(rng,100:Int(floor(typemax(Int64)/10000))) # Some RNG have problems with very small seeds.
    results = Vector{Float64}(undef,n_trees)
    Threads.@threads for i in 1:n_trees
        tsrng = StableRNGs.StableRNG(masterSeed+i*10)
        out = rand(tsrng) # assuming this is a stand-in for some long calculation
        results[i] = out
    end
    return results
end

```

Thanks @Mason for pointing out I forgot to delete an essential line.

---

<div class="post-metadata">

**Author:** ![foobar\_lv2](https://avatars.discourse-cdn.com/v4/letter/f/ee59a6/32.png) [@foobar\_lv2](https://discourse.julialang.org/u/foobar_lv2)\
**Post date:** [October 29, 2025, 12:00am UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/5 "2025-10-29T00:00:03Z")

</div>

You are explicitly using StableRNGs. This is necessary if you want exact reproducibility across julia versions.

If you don’t need that, julia’s built-in RNG is already doing what you want:

```julia-auto
function goo(outseed, n_trees)
    res = Vector{Float64}(undef,n_trees)
    task = @spawn begin
    Random.seed!(outseed)
    @sync for chunk in ChunkSplitters.index_chunks(1:n_trees; n = 128) #ok for up to 128 cores
        @spawn for idx in chunk
            res[idx] = rand()
        end
    end
    wait(task)
    res
end

```

I.e. it is designed to be deterministic unless you write racy code, and be independent of `nthreads` unless you use very legacy constructions like `@threads` that rely on `nthreads()`.

Above construction has the extra level of task-nesting to avoid overwriting the old RNG state.

> [@sylvaticus](#):
>
> (the +1 addition is new),

Unfortunately your code was already wrong in 1.10 and possibly in 1.9; start julia with `julia -t 4,4` to see the issue. Conversely, you can start julia with e.g. `julia -t 4,0` to avoid the +1 thing.

The issue is that julia has multiple threadpools, for interactive and batch-processing threads. Interactive threads come first in the numbering. Before julia 1.12, the default size of the interactive pool was size zero, now the default is one interactive thread; but command-line args or environment variables already could change that.

---

<div class="post-metadata">

**Author:** ![Joris\_Pinkse](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/joris_pinkse/32/216398_2.png) [@Joris\_Pinkse](https://discourse.julialang.org/u/Joris_Pinkse)\
**Post date:** [October 29, 2025, 2:00am UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/6 "2025-10-29T02:00:31Z")

</div>

Isn’t this scenario what `Random123` was designed to address?

---

<div class="post-metadata">

**Author:** ![sylvaticus](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/sylvaticus/32/203883_2.png) [@sylvaticus](https://discourse.julialang.org/u/sylvaticus)\
**Post date:** [October 29, 2025, 6:16am UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/7 "2025-10-29T06:16:02Z")

</div>

is there number of thread\*\*pools\* something that can change ? Currently there is a function `Threads.nthreadpools()` that returns 2 (for the default and interactive ones) but the fact itself that there is this function make me think that this is also something that could change.

---

<div class="post-metadata">

**Author:** ![WalterMadelim](https://avatars.discourse-cdn.com/v4/letter/w/3e96dc/32.png) [@WalterMadelim](https://discourse.julialang.org/u/WalterMadelim)\
**Post date:** [October 29, 2025, 8:35am UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/8 "2025-10-29T08:35:21Z")

</div>

I guess it’s not configurable by the user (e.g. you cannot let it be 3).

The related PR was

> <https://github.com/JuliaLang/julia/pull/42302#issuecomment-1072785204>
>
> Add an optional interactive threadpool.
> 
> \`Threads.@spawn\` now accepts an optio…nal first parameter: \`:default\` or \`:interactive\` (when unspecified, this is... \`:default\`). Interactive tasks should not perform high latency operations. Also added:
> \- \`Threads.nthreadpools()\`
> \- \`Threads.threadpool(tid = threadid())\`
> \- \`Threads.threadpool(t::Task)\`
> \- \`Threads.nthreads(\[:default|:interactive\])\`
> 
> Julia's \`-t\`/\`--threads\` command line argument (and also the \`JULIA\_NUM\_THREADS\` environment variable) may now be specified as: \`auto|N\[,auto|M\]\`, where \`N\` is the number of threads in the default threadpool and \`M\` is the number of threads in the interactive threadpool; \`N+M\` threads are started.
> 
> TODOs:
> \- \[X\] add test(s) (suggestions welcome!)
> \- \[X\] fix \`nthreads\_in\_pool()\` (not sure why it's broken)

---

<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:** [October 29, 2025, 9:37am UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/9 "2025-10-29T09:37:45Z")

</div>

> [@abraemer](#):
>
> If the calculation in the loop is sufficiently long (more than 10-100ms or so), I probably would not bother too much and prefer the simple version:
> 
> ```julia-auto
> function goo(outseed)
> rng = StableRNGs.StableRNG(outseed) # in the real app rng is choosen by the user
> n_trees = 4
> masterSeed =rand(rng,100:Int(floor(typemax(Int64)/10000))) # Some RNG have problems with very small seeds.
> results = Vector{Float64}(undef,n_trees)
> Threads.@threads for i in 1:n_trees
> tsrng = rngs[Threads.threadid()] # Thread safe random number generator
> tsrng = StableRNGs.StableRNG(masterSeed+i*10)
> out = rand(tsrng) # assuming this is a stand-in for some long calculation
> results[i] = out
> end
> return results
> end
> 
> ```

That isn’t thread safe.

* * *

Edit: Fixed now

---

<div class="post-metadata">

**Author:** ![sylvaticus](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/sylvaticus/32/203883_2.png) [@sylvaticus](https://discourse.julialang.org/u/sylvaticus)\
**Post date:** [October 29, 2025, 12:02pm UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/10 "2025-10-29T12:02:12Z")

</div>

So, something like this (to allow user choose the rng) ?

```julia-auto
function goo3(rng::Random.AbstractRNG = Random.GLOBAL_RNG)
    rng_type = typeof(rng) 
    n_trees = 4
    masterSeed =rand(rng,100:Int(floor(typemax(Int64)/10000))) # Some RNG have problems with very small seeds.
    results = Vector{Float64}(undef,n_trees)
    Threads.@threads for i in 1:n_trees
        tsrng = rng_type(masterSeed+i*10)
        out = rand(tsrng) # assuming this is a stand-in for some long calculation - yes :-)
        results[i] = out
    end
    return results
end

```

---

<div class="post-metadata">

**Author:** ![abraemer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/abraemer/32/51403_2.png) [@abraemer](https://discourse.julialang.org/u/abraemer)\
**Post date:** [October 29, 2025, 12:23pm UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/11 "2025-10-29T12:23:54Z")

</div>

It is probably a bit more general to make the process of “recreate this RNG with a new seed” a new function in case there are some special RNGs that require a different construction

```julia-auto
function copy_and_reseed(rng, seed) # maybe find a better name
    typeof(rng)(seed)
end

function goo4(rng::Random.AbstractRNG = Random.GLOBAL_RNG)
    n_trees = 4
    masterSeed =rand(rng,100:Int(floor(typemax(Int64)/10000))) # Some RNG have problems with very small seeds.
    results = Vector{Float64}(undef,n_trees)
    Threads.@threads for i in 1:n_trees
        tsrng = copy_and_reseed(rng, masterSeed+i*10)
        out = rand(tsrng) # assuming this is a stand-in for some long calculation - yes :-)
        results[i] = out
    end
    return results
end

```

This way you don’t require the RNG’s type to a have a constructor that takes a single seed. This should not show any performance difference and is just more flexible.

---

<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:** [October 29, 2025, 12:34pm UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/12 "2025-10-29T12:34:12Z")

</div>

> [@mbauman](#):
>
> You need task-local state, not thread-local state.

In 1.12, can’t you do

```julia-auto
rngs = OncePerTask{StableRNG}(() -> StableRNG(outseed))

```

and then do

```julia-auto
tsrng = rngs()

```

in the loop?

---

<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:** [October 29, 2025, 1:09pm UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/13 "2025-10-29T13:09:21Z")

</div>

> [@sylvaticus](#):
>
> So, something like this (to allow user choose the rng) ?
> 
> ```julia-auto
> function goo3(rng::Random.AbstractRNG = Random.GLOBAL_RNG)
> rng_type = typeof(rng) 
> n_trees = 4
> masterSeed =rand(rng,100:Int(floor(typemax(Int64)/10000))) # Some RNG have problems with very small seeds.
> results = Vector{Float64}(undef,n_trees)
> Threads.@threads for i in 1:n_trees
> tsrng = rng_type(masterSeed+i*10)
> out = rand(tsrng) # assuming this is a stand-in for some long calculation - yes :-)
> results[i] = out
> end
> return results
> end
> 
> ```

I guess, but this seems pretty brittle for a generic method. I don’t think you’re not really guaranteed that `typeof(rng)(seed)` is the right way to construct the RNG object in general.

I saw before you used `deepcopy`, but I think isntead it should be just regular `copy`, since `deepcopy` is really dangerous. I’d suggest something like

```julia-auto
#------------------------------------------------v use default_rng instead of GLOBAL_RNG
function goo4(rng::Random.AbstractRNG = Random.default_rng())
    n_trees = 4
    masterSeed =rand(rng,100:Int(floor(typemax(Int64)/10000))) # Some RNG have problems with very small seeds.
    results = Vector{Float64}(undef,n_trees)
    Threads.@threads for i in 1:n_trees
#------------------v copy the rng and seed it instead of applying it's type to the seed
        tsrng = copy(rng)
        Random.seed!(tsrng, masterSeed+i*10)
        out = rand(tsrng) # assuming this is a stand-in for some long calculation - yes :-)
        results[i] = out
    end
    return results
end

```

If you want to avoid so much copying / reconstruction of the RNG, you could do some manual `@spawn`-ing like so:

```julia-auto
using ChunkSplitters
function goo4(rng::Random.AbstractRNG = Random.default_rng(); ntasks=Threads.nthreads())
    n_trees = 4
    masterSeed =rand(rng,100:Int(floor(typemax(Int64)/10000))) # Some RNG have problems with very small seeds.
    results = Vector{Float64}(undef,n_trees)
    # Split 1:n_trees up into a number of chunks equal to ntasks
    tasks = map(Iterators.partition(1:n_tress, n_trees÷ntasks)) do chunk
        # Spawn a task that does one unit of parallel work
        Threads.@spawn begin
            # Seed the rng *before* looping so it only happens once per task
            tsrng = copy(rng)
            Random.seed!(tsrng, masterSeed+first(chunk)*10)
            for i in chunk
                out = rand(tsrng) # assuming this is a stand-in for some long calculation - yes :-)
                results[i] = out
            end
        end
    end
    foreach(wait, tasks) # wait for all the tasks to finish
    return results
end

```

This gives you a bit more control over what’s going on, and makes it so you only copy the RNG and re-seed it once per task. ChunkSplitters.jl will also provide a better alternative to `Iterators.partition` if you’re okay with adding a dependency.

---

<div class="post-metadata">

**Author:** ![foobar\_lv2](https://avatars.discourse-cdn.com/v4/letter/f/ee59a6/32.png) [@foobar\_lv2](https://discourse.julialang.org/u/foobar_lv2)\
**Post date:** [October 29, 2025, 4:52pm UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/14 "2025-10-29T16:52:53Z")

</div>

> [@stevengj](#):
>
> `rngs = OncePerTask{StableRNG}(() -> StableRNG(outseed))`

This has the issue that all tasks will get the same seed → collisions.

You really need some deterministic name / number for each task that you mix into the seed.

The default RNG in julia uses the structure of tasks: All except for the root Task have a parent; and all children of a Task are created in a sequence, so each Task has a “pedigree” string like “[5, 1, 3]”, which means “third child of the first child of the fifth child of the root task”; and that is the thing used.

---

<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:** [October 29, 2025, 7:25pm UTC](https://discourse.julialang.org/t/is-this-pattern-still-thread-safe-in-julia-1-12/133484/16 "2025-10-29T19:25:43Z")

</div>

> [@foobar\_lv2](#):
>
> This has the issue that all tasks will get the same seed → collisions.

The OP is resetting the seed to a different (deterministic) value for each loop iteration, so the initial seed of each task should be irrelevant.

My point here is that `OncePerTask` mimics what seems to be the intended behavior of the OP’s original code, except without the lack of thread safety of using a per-thread array. Whether that intended behavior is a good idea is a separate question.
