Multithreading in Julia >1.8 - Threadsafety of @threads vs @spawn

Hi there,

I have recently read the blog post about changes in @threads behavior from version 1.8 onwards: PSA: Thread-local state is no longer recommended, and I am unsure if I do need to change up code that I wrote for a previous Julia version.

  1. In my current code, I have multiple functions that use @threads without using the stated threadid, but I am using preallocated buffers that are filled during a loop.
  2. I dont care at which index that buffer is filled.
  3. The function inside the @threads loop mutates my arguments, but again I do not care in which order the arguments are getting mutated.

All my code has the following form:

Nchains = 96
# Nthreads == 8
Niterations = 10
T = 1000

for idx in 1:T
    buffer_argument1 = [BufferArgument1() for 1:Nchains]
    buffer_argument2 = [BufferArgument2() for 1:Nchains]
    buffer_output = [BufferOutput() for 1:Nchains]
    Base.Threads.@threads for Nchain in Base.OneTo(Nchains)
        for iter in 1:10
            buffer_output[Nchain] = func1!(buffer_argument1[Nchain], buffer_argument2[Nchain])

Do I need to change @threads to a version working with @spawn, or is this safe going forward? I tried to find the solution for this based on the multiple open questions already on discourse, but am still unsure about the solution.

Your pattern should be safe: multi-thread over Nchains many items, and you allocate all state Nchains many times.

The problem is with a different kind of pattern: In many cases, one needs large amounts of reusable temporary internal state, and it would be prohibitive to allocate that Nchains many times. Instead, one wants to allocate as few as possible instances of the internal state and reuse them. The old canonical solution was tmp_states = [makeTmpState() for i=1:Threads.nthreads()] and later use tmp_state = tmp_states[Threads.threadid()] and that is what got broken.

As far as I know, there exists no canonical solution yet (i.e. the community has not yet found consensus what the standard solution should be).


That makes sense, thank you! As long as I do not depend on internal instances, I should be safe. Just to finalize this: If I had, as an additional argument, a shared argument that is not mutated during the loop would this create any additional allocations?

For example:

Nchains = 96
shared_argument = SharedArgument()
buffer_output = [BufferOutput() for 1:Nchains]

Base.Threads.@threads for Nchain in Base.OneTo(Nchains)
     buffer_output[Nchain] = func1(shared_argument)

That is completely safe and fine and performant.

1 Like