Task scheduling semantics

Reading through https://github.com/JuliaLang/julia/pull/40715 I believe task migration across threads is primed for 1.7 release. Very much looking forward to this, but also have some concerns around the clarity in semantics of task scheduling.

@async launches a task that schedules on and sticks to the current thread. How will this look after the task migration change?

In my view there are two kinds of async-non-concurrent execution you want in the system. For interfacing with external libraries or directly with thread local store, you may need tasks that stick to the current thread. For application layer async code (e.g., to overlap IO) what you typically want is to guarantee non-simultaneous execution of a set of tasks in a group (e.g., with the parent task and its children), however you aren’t restricted from these migrating across threads. I can see a need for both in my own use cases, so either the API needs to be extended to support this distinction, or task migration needs to be prohibited if a task launches any nested sticky tasks.

I’m curious what the plans are and everyone else’s thoughts. On the same token, perhaps the task scheduling macro API could be simplified and cleaned up; for instance with a single macro taking a scheduling policy argument, rather than a macro per use case. Scheduling policies I can think of would be unrestricted, bind-to-current-thread, bind-to-launch-thread, bind-to-parent-thread.

2 Likes

I needed to know about the new behavior but was also confused about where to look. To link up some information I found:

The co-scheduling behavior of @async tasks with their parent tasks was discussed in

https://github.com/JuliaLang/julia/issues/41324

For 1.7, it was decided that unsticky tasks become sticky if they launch any children with @async, thus ensuring that @async tasks always run on the same thread as their parent.

https://github.com/JuliaLang/julia/pull/41334

This probably isn’t a long term solution, it’s more of a workaround to ensure existing code doesn’t break.

2 Likes

Thanks :slight_smile: Yes that first issue is one I posted, and the second spawned out of that discussion. Longer term a stronger API for more advanced control would be great.

Yes, thanks for pushing on that. A fair amount of concurrent code would have been broken without the coscheduling workaround.

What happens when the root task is yielded? Can a task created with Base.Threads.@spawn be rescheduled on the main thread in this case, or can the main thread only run tasks created with @async from some task already running on the main thread?

The main thread can pick up tasks like any other. ThreadPools.jl has some utilities to schedule away from the main thread.

Thanks for the information and the tip. It’s a rather painful situation for my planned use-case (I want to wait on an AsyncCondition that is notified from Rust with uv_async_send whenever Rust needs to reclaim control of the main thread so spamming jl_process_events can be avoided and other tasks scheduled on the main thread can be executed), but such is life :slight_smile: