Task queueing best practices

I’m making an in-editor tool which will frequently need to perform large processing tasks. I’d like to do them asynchronously on a separate thread (or threads), to keep the editor as responsive as possible. My assumption is that the task system is best for this, particularly since the beginning and end bits of each process do need to execute on the GameThread.

My questions:

  1. Is there any benefit to creating a separate thread or threads specifically to handle the tasks from my tool? If so, should that be done by creating a new ENamedThread enumerant, or in some other way?
  2. How many worker threads are there to handle tasks which are dispatched for ENamedThreads::AnyThread? Is that number controllable?
  3. Will long-running tasks on GameThread block the editor’s responsiveness? Will they block the responsiveness of editor tasks, such as moving objects in the world? Will calling FGraphEvent::DontCompleteUntil from such a task block other tasks on the GameThread until the waited-for event fires?

Hi Sneftel,

The Task Graph system works best for small units of work that finish very quickly. For larger, long running tasks - especially tasks spanning several frames - you’re probably better off spawning a separate worker thread using the FRunnableThread API.

The number of task graph threads created depends on a number of factors, most importantly the number of available CPU cores. You can find the current logic for this in the FTaskGraphImplementation constructor. Adding your own task graph thread probably doesn’t make much sense, since we already try to spawn the optimal number of threads.

If you schedule a long running task on the GameThread, then yes, it will block the game’s/Editor’s responsiveness. In fact, no work will be done in the game/Editor until your task has completed, and no user interaction will be possible. This is generally not desirable. If you schedule a long running task on a non-Game thread, then it will, too, block whichever thread it was scheduled to. DontCompleteUntil() will also block the task’s completion until some event happened. No other tasks will be executed until the current task has completed.

To sum it up, we do not recommend the task graph for long running tasks. Use a separate OS thread using FRunnableThread to perform the work instead. You can regularly check on the game thread, i.e. in Tick(), whether this thread completed. If you have to regularly execute work on such a separate thread, it may also be a good idea to not recreate the thread each time, but instead leave it running and have it Sleep() when there is no work. You can use a work queue (TQueue) in combination with an event (FEvent) to wake up the thread and perform work. There are some examples of this in the code base, i.e. FMessageRouter::Run().

DontCompleteUntil() will also block the task’s completion until some event happened. No other tasks will be executed until the current task has completed.

Is this still valid? I am asking because I am using DontCompleteUntil in tasks running on the game thread (UE 4.20), and passing a FGraphEvent to DontCompleteUntil does not seem to stall the thread, even if it takes a while before that event gets DispatchSubsequents called on it.

The number of task graph threads created depends on a number of factors, most importantly the number of available CPU cores. You can find the current logic for this in the FTaskGraphImplementation constructor. Adding your own task graph thread probably doesn’t make much sense, since we already try to spawn the optimal number of threads.

There are situations where decreasing the number of task threads would be beneficial, say you are running a docker container that only has access to 6 out of the 8 cores on the system as you’ve pinned 2 cores to the system. UE4 is going to see 8 cores and think, great I’ll leave one of these for the OS, 1 for the game thread, 1 for the physics thread and I’ll have 5 available for TaskGraph, when in fact it is only going to have 4 cpu’s available for 5 async task threads, meaning 2 of those threads are going to be sharing the same core, which is terrible.

Soi I’m hoping that one can into the FTaskGraphImplementation constructor and manually set how many cores it should grab?