It is strongly encouraged to favor Threads.@spawn over @async always even when no parallelism is required especially in publicly distributed libraries. This is because a use of @async disables the migration of the parent task across worker threads in the current implementation of Julia. Thus, seemingly innocent use of @async in a library function can have a large impact on the performance of very different parts of user applications.
What async does is simply scheduling the thread in a local machine.
What’s happening?
Is this going to be fixed in the future?
When to use async vs spawn?
My mental model was that async is similar to a green thread whereas spawn actually spawns another thread but what actually happened?
@async and @spawn actually do almost the very same thing: They wrap the contained code in a Task and schedule it. The only difference is in the scheduling. @async sets the sticky flag, which means the Task is not allowed to migrate to another thread and this then extends to the parent Task that spawned (and its parent and so on). This is generally unwanted because it affects load-balancing. Julia is actually one of very few languages where threading composes well due to its dynamic, depth-first scheduler but that requires Tasks to be able to change their worker threads. Read more about it here:
To summarize: The core problem is that @async can cause tremendous slowdowns in unrelated code parts because it disables migration of Tasks which is needed for efficient, composable multi-threading.
This model is wrong. Julia never adds threads dynamically. There is a fixed number of worker threads that switch between all scheduled Tasks. You could view the creation of a Task as similar to spawning a green thread.
The number of threads in Julia can change, but currently Julia doesn’t dynamically add threads, the only way for adding additional threads is for so called “foreign thread adoption”. E.g. a C/C++ library using it’s own threads underneath and ending up calling back into Julia. That thread will get adopted into Julia and so you may have Base.Threads.nthreads() being different than Base.Threads.maxthreadid().
So are there cases where @async would still be preferred over @spawn in order to avoid race conditions? E.g. if you have a parallel mutations of arrays that aren’t thread safe as noted by stevengj here. Sorry i’m still a little confused about this point.
Not really no as you have no guarantees where a Task pauses (AFAIK). It might be that in practice you will avoid some race conditions (e.g. if the compiler never inserts a yield into something like array[i] += value making it practically an atomic operation). But I’d never want to rely on this! Julia has no way of controlling the yield points so you always had to guess where they are and hope - which is not how I would want to construct my programs