Type stability and tasks

I’m looking into using tasks for some parallelization speedup, but I’m worried about type stability in the returned objects. For example, I’m calling a type-stable function over a range of Ints; the returned objects should all have the same type. All the discussion I’ve seen about this sort of thing is pretty old. Has Julia gotten better at inference? Is StableTasks.jl the thing to use?

You can use StableTask or wrap the threaded part into a function and force the return type it may work. It shouldn’t really mater though.

I would just use OhMyThreads.jl which do that in a safe way and gives you more control. Its also the recommended way now.

Julia’s type inference mechanism isn’t actually the bottleneck here. If an unparametric Task type wraps any (0-argument) function call with any return type, then functions like fetch that map a Task to return values are inherently type-unstable: fetch(::Task)::Any. You always could help the compiler afterward with type assertions like:

foo() = fetch(Threads.@spawn 1+1)::Int # either returns Int or errors

…but there is still that bit of fetch(::Task)::Any requiring a runtime type check Core.typeassert(::Any, Int). You can verify with @code_warntype and @code_llvm.

That said, you probably knew that already, and what you really meant to ask is if Julia has introduced parametric tasks like StableTasks into the tasks API. No, and since Task is documented for the existing API, it would take new signatures or calls like StableTasks. And that leads to another implied question: practical Julia is usually very insistent on type stability, so how were people using nonparametric Tasks this whole time? fetch(::Task) isn’t the go-to approach; the simplest case of a call fetching from one task is idling a call or task just to schedule another task, which isn’t a throughput improvement over just calling the underlying function (though more divided Tasks generally improve concurrency). Instead, tasks share memory via Channels, locks, and atomics, which can be type-stable. StableTasks’ internal metaprogramming actually makes the fetch-able StableTask{T} and the internal Task body share atomic memory @atomic x::T. You could write that directly, but the abstraction of StableTasks can be an easier edit and is open to internal improvements.

I guess it’s important to mention that the most implementations of Task like thing would force boxing the value and something like foo() = fetch(Threads.@spawn 1+1)::Int has little runtime cost as compared to all the other overheads of spawning and scheduling and waiting on a task. (A type assert like that is usually 2 cpu instructions.