New Core Feature: Async Framework (Master Branch and 4.8)

Sure!

Note: I’ve changed the names of many of my classes to conceal the purpose of this code, and I may have accidently chosen some that are already in use. They’re generic and stupid, so please pick better ones for your own code! Also, this example is a little too boilerplate - it’s how I did it, not how you /could/ do it in a general sense, so don’t feel the need to copy this approach directly (wrapper structs etc).


I have a struct that wraps my callback (it has some data that the worker uses, for now let’s assume that the in-data is a TArray of FStrings.



struct FAsyncDataRequest
{
	FAsyncDataRequest() {}

// callback is set manually, not by constructor (long story)
	FAsyncDataRequest(TArray<FString> a_meshIDs)
		: m_meshIDs(a_meshIDs) {}

	TArray<FString> m_meshIDs;
	FOnAsyncDataLoaded m_callback;
};


I pass this in to a worker that inherits from FNonAbandonableTask.



class FAsyncDataWorker : public FNonAbandonableTask
{
public:
	FOnAsyncDataLoaded OnLoadFinishedCallback; // This is the delegate called on the main thread when the data has been loaded.
		
public:
	FAsyncDataWorker ();
	~FAsyncDataWorker ();

// The owner of this worker pulled the data from the request and passed it in directly
	FAsyncDataWorker (TArray<FString> a_meshIDs, FString a_dataFilesDir); 

	void DoWork();
	TStatId GetStatId() const;

private:

// this is the function actually called by the taskgraph system to hop from the worker thread back to the main
// the data result struct just holds the results of this worker's DoWork (in my case, a TArray of structs).
	void OnLoadFinished(FAsyncDataResult a_dataLoadResult); 

	TArray<FString> m_meshIDs;
	FString m_dataFilesDir;
};

I also have a quick typedef, since you need to have an FAsyncTask template with your worker class that inherits from FNonAbandonableTask (it doesn’t have to inherit, but it’s easier using this stub I suppose).


typedef FAsyncTask<FAsyncDataWorker > FAsyncDataTask;

This is how the actual work begins:



// You can pass arguments into this constructor that are given to the FAsyncDataWorker's constructor
// since they're using a templated 'pass-through' constructor. Pretend that you're constructing the worker
// directly
FAsyncDataTask* asyncTask = new FAsyncDataTask(a_asyncDataRequest.m_meshIDs, m_baseDataPath);
// I bind the callback that will be executed on the main thread after load
asyncTask->GetTask().OnLoadFinishedCallback = a_asyncDataRequest.m_callback;
asyncTask->StartBackgroundTask();


This will start the FAsyncDataWorker::DoWork() function, which essentially does this:



void FAsyncDataWorker:: DoWork()
{

	FAsyncDataResult loadResult;
	MyDataLoaderClass dataLoader(m_dataFilesDir);
// the data loader returns my TArray of structs by reference
	dataLoader.LoadMyData(m_meshIDs, loadResult.TheTArrayOfStructs);

	FGraphEventRef ref = FSimpleDelegateGraphTask::CreateAndDispatchWhenReady
		(
			// works fine, but requires us to specify and create the delegate here, rather than
			// elsewhere and passing it in.
			FSimpleDelegateGraphTask::FDelegate::CreateRaw(this, &FAsyncDataWorker::OnLoadFinished, loadResult),
			TStatId(),
			nullptr,
			ENamedThreads::GameThread
		);
}


The delegate created above will be called back on the main thread, and will have the data load result as an argument. This ‘OnLoadFinished’ is essentially a function internal to our worker. All it does is the following:


void FAsyncDataWorker::OnLoadFinished(FAsyncDataResult a_loadResult)
{
		if (!OnLoadFinishedCallback.ExecuteIfBound(a_loadResult))
	{
		UE_LOG(LogTemp, Warning, TEXT("Failed to execute delegate when finished loading data in FAsyncDataWorker."));
	}
}

Now my original delegate is execute on the main thread, bound to a class about which this worker does not know, and I get the data I’ve loaded onto the main thread!


Remember, I had to change the names here due to it otherwise being obvious what I’m working on. Some of these names might be taken, so pick new ones intelligently :slight_smile: I may have screwed them up a little, too, so let me know.

Specify task graph thread?

Any way we could get an option in when using EAsyncExecution::TaskGraph to optionally specify a named thread? That way we could send tasks that are only safe to run on the game thread, to the game thread.

Just to enable things like this without as much boilerplate: Thread safety, delegates and bPostTickComponentUpdate - Programming & Scripting - Unreal Engine Forums

So, in 4.13 there will be the AsyncTask function, which does exactly what you want. That being said, I’m not too happy with the name (should probably be ExecuteOnThread), and there are some concerns about wrapping the task graph system in this way. You can start using it, but it needs a little more thought, and it may change in the near future.

What we really want is an API for continuations similar to what is possible in .NET and some JavaScript frameworks (DoThis(…).ThenThat(…).AndThis(…).OrThat(…) - you get the idea). It is not yet clear how this would be implemented. The CompletionCallback on Async and the new AsyncThread is also a bit of a hack and not all that useful. I need to find some time to think about this some more and provide a cleaner and more useful API for chaining multiple tasks. Consider the current features as a stepping stone in that direction.

I don’t suppose you could whip up an example of adapting this for use with IHttpRequest, could you? At the moment I have this horrible nonsense:


TFuture<FXmlFile*> FOsmClient::GetCapabilities()
{
	return Async<FXmlFile*>(EAsyncExecution::Thread, &]() -> FXmlFile* {
		TSharedRef<IHttpRequest> Request = HttpModule->CreateRequest();
		Request->SetURL("http//api.openstreetmaps.org/api/capabilities");
		Request->SetVerb("GET");
		Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent");
		Request->SetHeader(TEXT("Content-Type"), TEXT("application/xml"));
		Request->ProcessRequest();

		FHttpResponsePtr Response;
		while (!(Response = Request->GetResponse()).IsValid()) { }

		const FString Str = Response->GetContentAsString();
		FXmlFile* Content = new FXmlFile(Str, EConstructMethod::ConstructFromBuffer);
		return Content;
	});
}

Request is already Async. If you look into the source code in OSS implementation for example, you will see that you can bind OnProcessRequestComplete delegate.
So no need for a while loop… and Async method.

Is there any update on how continuations are going?

No progress yet, sorry. I have no ETA at the moment. I doubt that I’ll get to this before December. Either way, 4.14 is already branched, and 4.15 is not going to be until February anyway.

I’m a newbie to threads and such stuff … I was profiling my 1st game and ended up here while trying to optimize my game performance. But i’m interested in this topic. Where should I look for basic info about threads, async event n stuff. I’m using pure BP for now but this info won’t be useless for me later.

Hi,

this works for me:

Is there any info about UObject being able to be accessed from other threads/run functions on other threads and being created from other threads ?
The last information about it I found is from 2014 and it “working on making UObject thread safe”. but it doesn’t seem to have any progress since then.

I’d like to know if anything has changed as well… Last time I checked UObjects can’t be touched outside game thread.

UObject creation and destruction is thread-safe. However, this doesn’t mean that UObject usage in general is thread-safe. You are still responsible for writing thread-safe code yourself. It is probably safe to assume that none of the built-in methods are thread-safe.

@gmpreussner, can you elaborate a bit on what that means in practice?

You’re saying it’s okay to call NewObject outside of the game thread? If so that’s news to me. Is there some synchronization going on to prevent the GC from snaffling the object before we have chance to root it or reference it somewhere?

And then what about safely setting a UObject* property somewhere, since the GC could be reading those at any time, right?

I’d previously assumed the only potentially safe way to use a UObject in a background thread was to pass a pointer from the game thread to an existing object that you know is guaranteed to remain alive, and even then to pretty much stick to only accessing code and member variables that we control ourselves.

I’m really interested in this feature. I am trying to create 8 TaskGraphs and in each one, I create 4 TaskGraphs. Do you think this way may cause some problems? So can I create some TaskGraphs in another TaskGraph?

Should I used Async when I don’t care about the result?

Any news on the continuations? Coming from web programming and C# (where we already have both chained promises and the awesome async-await), it feels incredibly hard and time-consuming to implement asynchronous and optionally-parallel computations in UE4. We’re in C++, though, and I understand how hard it most certainly is to implement such a thing so that it works on at least most platforms consistently.

Question - I understand it’s not desirable and safe to change any properties of uobjects in async tasks and other threads, but how about just accessing their properites?
Example: I have a complex perception and goal prioritization AI system, which takes all the available (overlapping, for example) actors, extracts possible actions from their components and ranks them based on their weight (which is determined by const function calls, so no side effects, only calculations). In crowds, it becomes incredibly slow to spawn any new AIs, cause they start studying and ranking all 100+ of other guys around them and pretty much freeze the game thread.

I feel like it would be a perfect thing to offload the whole evaluation on separate thread(s) and call delegate to make AI decision as soon as all evaluation is done. However, the evaluation “reads” overlaps, actor locations etc. I have not yet attempted to try and make it work, but do you think there are any problems with this approach?

I’ve found it impossible to get any real information out of Epic staff on even just a basic idea of what is and isn’t safe regarding UObjects and threads.

Still, I think it’s fair to say that even reading properties isn’t safe to do from another thread unless those properties are your own and you can guarantee they’re not being written to on some other thread. In the case of things like component transforms this clearly isn’t the case - the engine may be updating them, so you can’t just read them from a background thread. What you’d want to do is have some code being run on the game thread that grabs the data you need and hands a copy of it off to the worker thread to process.

What you can’t do:

  • Create Objects.
  • Destroy Objects.
  • Change Object Allocation Size.
  • Invoke or Broadcast Delegates.
  • Broadcast BP Events that belong to Game Thread.
  • Create Slate Widgets, etc, etc…

What you CAN do:

Setup your thread/task to avoid ‘race condition’.
Then you can do things like…

  • Message Game Thread to create a UObject “when possible”.
  • The same to destroy an Object, add a message to queue.
  • Directly change allocation size and value of UProperty containers when using UProperty Helpers.
  • Message Game Thread to invoke Delegates “when ready”.

Depending on the size of your “task”, when you message Game Thread, it may lock or slowdown if your thread is sending tons of messages every frame because then they won’t in essence be asynchronous threads anymore.

Care to reference where you’re getting this information from? I’m curious because re. creation/destruction you’ve directly contradicted what an Epic engineer has written just a few posts up. Also the idea that you can’t invoke delegates outside of the game thread makes no sense at all.

With “UObject” I mean AActors, UActorComponents, etc… these more complex classes.
If you invoke multicast delegates from outside game’s thread that runs functions within Game Thread all you’ll get is a call to a check(IsInGameThread()) and Unreal gonna crash.

I am currently writing multi-threaded modules (some for plugins too).
If you create basic UObjects, it’s fine, but more complex classes will pretty much always cause fatal issues.