So I want to compute many things, lets say a couple thousand, off the game thread and on as many cores as possible. It’s okay if this process takes several frames/ticks. (In Unity this would be a IParallelFor Job.)
What would be the best tool within Unreal to use to accomplish this?
Looking through the documentation I see…
FRunnable: Every example use is running a single process on a separate thread.
FQueuedThreadPool: Sounds reasonable, but I can’t find a single example of this being used.
FAsyncTask: Like FRunnable, every example I have found is only running a single other thread.
Task System: This claims to be an improvement of the TaskGraph, but doesn’t include any clear examples on running multiple parallel tasks.
TaskGraph: There are examples of using the TaskGraph in such a way. But from what I’ve read it’s not clear whether these can run asynchronously over multiple ticks or will stall if not completed within a single tick.
So what tool should I be using for this? Is there any examples I can learn from? Thanks.
@protoj18
I’ve only ever really followed the FRunnable code in the wikis - is there something about the other options that aren’t working for you?
I can really only give you a tutorial I’ve stumbled upon:
Forgive me if I am misunderstanding this example, but it seems as if it is only creating a single thread to be continually run while not interrupting the main game thread.
To explain how I understand it further the function JoyThread_Init() creates the an object named URamaThreadDataQueue. But this isn’t a queue of threads, it’s a queue of thread data. Meaning that once started this single created thread will continually run and calculate prime numbers via JoyThread_CalcNextPrimeNumber() and then enqueue them into this data queue via RamaCodePrimeCalcThread (Line 336: MultiThreadDataCore->Responses.Enqueue(ThreadCalcedData); )
Now while this is a fine example of using FRunnable to create a single thread and then have it continually run (and get data from them as they complete), it is not the same as creating a thousand jobs and putting them in a thread queue to be allocated to multiple cores as they become available. Which is fairly common practice in multithreaded programming, and frankly a necessity for most complex procedural mesh creation.
I hope I am explaining the difference well enough. The fact that there is a class called FQueuedThreadPool makes it seem like this is within the bounds of what unreal can do. But there doesn’t seem to be a single example or tutorial or clear documentation about using it. Or perhaps that tool isn’t for that and something else is. I don’t know as I haven’t seen a single example of this in unreal, and all the documentation is very sparse. Is there anybody who has seen a use case like this (a pool of many jobs allocated to threads as they become available) implemented in unreal engine? It is strange to me that this would be so hard to find.
Ok, so I guess this is something no one can help me with?
I did some more googling and reading to see if there was anything out there at all I could use. Figured I’d update this in case anyone else wants to follow along the struggle to use this fairly basic multi-threading functionality in Unreal.
What I’ve gathered so far is that FQueuedThreadPool would be what I would want. This thread pool implementation is built on top of FRunnable/FRunnableThread, which compile out to the native system’s thread implementation. One article (in Chinese) even mentioned “UE4 has also done a lot of work on the performance optimization of thread pools.” Would be great if developers could easily access that performance. Oh well.
Seems the way to do that is create a FQueuedThreadPoolBase object and make use the ‘Create(uint32 InNumQueuedThreads, uint32 StackSize, EThreadPriority ThreadPriority, const TCHAR* Name)’ and ‘AddQueuedWork(IQueuedWork* InQueuedWork, EQueuedWorkPriority InQueuedWorkPriority)’ functions. Still looking for any example of implementing the IQueuedWork interface. Also would be great to see these actually return data, as I’ll need that eventually. Looks like I might have to trial and error my completely through that on my own.
As for the Task System, in still remains unclear whether it can be run over multiple frames. Still might need to look into this.
TBH the most complex multithreading I’ve done in UE was setting up a loading screen, so I can’t help much, but this documentation seems the most relevant to your needs.
I did see that before. Unfortunately this too only seems to run one task in one thread. The nested tasks and pipelines they reference being used are to control the execution order of what happens in a single thread. This is different from assigning many functions to tasks as they become available.
Of course it does say the Task System “improves on the TaskGraph” in the documentation provided. But then, at least according to this (Multi-Threading: Task Graph System | Unreal Engine Community Wiki) the Task Graph “allows you to perform many actions, each of which is relatively small, on separate threads from the game thread” which would mean the Task System isn’t really an ‘improvement’ but more so just a different kind of solution for a different kind of problem. Which is confusing at best, and downright bad documentation at worst.
Lest anyone think it becomes clear the Task Graph system is the solution, the documentation from Unreal for how to use that doesn’t seem to exist and further that wiki article goes on to say “Please note that a task graph will sometimes use the game thread, and therefore for large tasks you should use a FRunnable as demonstrated in my other multi threading wiki.”. Which, if true, makes Task Graphs seem unlikely to be the solution.
Of course, is does give a link to an actual solution which is, of course, dead. Searching the wiki we can find this (Multi-Threading: How to Create Threads in UE4 | Unreal Engine Community Wiki) which only brings us once again to taking a single sequential function, aka sequentially finding prime numbers on one separate thread and we are right back to where we started.
That’s true. This would probably be a good starting point for using the FQueuedThreadPool.
That said, before trying that, I did end up finding an alternative solution through a combination of Tasks and ParallelFor calls that seems like it should give me everything I need.
Stumbled onto your post. I haven’t looked into beyond what my use case was, but TGraphTasks may well be what you need. They have a “GetDesiredThread” method. Here’s a snippet of my own implementation
### Header
class FAsyncGraphTask_PurposeSelected
{
protected:
TObjectPtr<UContextData> contextData;
FPurposeEvaluationThread& callingThread;///Used to detect stopThread
bool shouldAbandon = false;
public:
FAsyncGraphTask_PurposeSelected(TObjectPtr<UContextData> inContext, FPurposeEvaluationThread& inCallingThread)
: contextData(inContext),
callingThread(inCallingThread)
{
callingThread.purposeSelectionTasks.Add(this);/// this exists so that we may set shouldAbandon when thread is being stopped and destroyed, otherwise Async tasks will attempt to be performed on shutdown
}
virtual ~FAsyncGraphTask_PurposeSelected()
{
//Global::Log(EHierarchicalCalltraceVerbosity::DEBUG, "FAsyncGraphTask_PurposeSelected", "~FAsyncGraphTask_PurposeSelected", TEXT(""));
if (IsValid(contextData)) { contextData->RemoveFromRoot(); }/// Ensure context is removed from the root set otherwise we'll crash
callingThread.purposeSelectionTasks.Remove(this);
}
FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FAsyncGraphTask_PurposeSelected, STATGROUP_TaskGraphTasks); }
static FORCEINLINE ENamedThreads::Type GetDesiredThread() { return ENamedThreads::GameThread; }
static FORCEINLINE ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::FireAndForget; }
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
//Global::Log(EHierarchicalCalltraceVerbosity::DEBUG, "FAsyncGraphTask_PurposeSelected", "DoTask", TEXT(""));
shouldAbandon ? Cancel() : PurposeSelected();
}
void PurposeSelected();
/// Can be called by any thread in order to ensure that the purpose chain halts
/// When the task attempts to DoTask(), it will instead cancel
void Abandon() { shouldAbandon = true; }
protected:
void Cancel()
{
// //Global::Log(EHierarchicalCalltraceVerbosity::DEBUG, "FAsyncGraphTask_PurposeSelected", "Cancel", TEXT(""));
contextData->RemoveFromRoot();
}
};
};
### Execution from CPP
TGraphTask<FAsyncGraphTask_PurposeSelected>::CreateTask().ConstructAndDispatchWhenReady(context, *this);
Good article here Multithreading and Performance in Unreal - nice overview of multithreading tools in UE.
“seem like this is within the bounds of what unreal can do” - oh boy, you still have Unity mindset. “What Unreal can do” - you’re programming in pure c++, you don’t care what “Unreal can do”, you’re not bounded by the engine like in Unity. If you don’t like how Unreal does multithreading - no one stops you from doing it in pure c++.