Are generated functions threadsafe?

Is it true that generated functions (not their bodies) always run on the main thread? I had assumed so, but now that I think of it I’m not sure I’ve ever seen that actually confirmed, so just wanted to verify. Basically, could the following ever finish with x==1 because of a race condition in the non-atomic x+=1?

x = 0

@generated foo(::Int)    = (global x+=1)
@generated foo(::String) = (global x+=1)

Base.Threads.@spawn foo(1)
Base.Threads.@spawn foo("two")

x # could this ever be 1 here? 

According to the documentation:

Generated functions must not mutate or observe any non-constant global state (including, for example, IO, locks, non-local dictionaries, or using hasmethod ). This means they can only read global constants, and cannot have any side effects. In other words, they must be completely pure.

So if your generated function reads or modifies a global variable, the behavior is undefined.

7 Likes

Hmm good point, I suppose if they must truly be completely pure then my question is moot since I can’t come up with an example where the thread its running on actually matters, can I? And I guess that’s the point, since its what, in theory, could let code generation become multi-threaded in the future?

Despite that I’ve read those docs before, I guess I hadn’t appreciated that I was breaking these rules in GitHub - marius311/Memoization.jl: Easily and efficiently memoize any function, closure, or callable object in Julia. (which prompted this question) which uses a const caches = IdDict() to store memoization caches for each memoized function and uses a generated function to move the lookup for a given function to compile time. Guess I may have to rethink that design for a future version of Julia (works great for now though…)

I think this limitation also comes from the fact that the compiler might decide to generate the function body several times. In other words, it does not guarantee that the function body will be generated only once per argument type.

I think this aspect of the issue is taken care of in Memoization.jl by get_cache handling not only the case where the cache for a new function must be created, but also the case where a cache already exists for the function. The latter will occur if the compiler decides to re-generate the get_cache body for a function it has already seen.

2 Likes

That’s right. I can’t think of where that could go wrong, but I guess I’m a little worried to rely on what I now realize is undefined behavior.