Recently I was working on some editor scripts that do a large amount of processing on assets; in this particular case, exporting and importing asset data to and from a csv respectively. OK, not too hard, just grab all the assets of a type and serialize the data to csv. The hard part comes from doing it in a way that doesn’t block Unity’s UI thread.
I originally wrote the code as you normally would, but when I decided I wanted to add a progress bar to the UI, things got a bit more complicated. I knew already that Unity APIs are not thread safe and must be called from the main application thread, however due to the nature of C#, some properties you may not suspect of being problematic are still at their core, an API call.
In the csv output, the first column is the asset name for the data being serialized. So you may do something like this when serializing the line:
string line = assetList[i].name + ",";
This line however makes an API call. The name of any object is actually a C# property, which underneath makes a Unity API call to retrieve the name of the asset. So how do you get around this? The answer itself is fairly simple, but far from obvious to most beginners. If the API must be called on the main thread, well you need to tell the main thread to make the call and give you the value back. But how do you do that cleanly?
The answer here is fairly elegant, C# provides all the tools you need to do this. Take a look at this new line:
string line = await RunOnMainThread(new Task<string>(()=>assetList[i].name));
line += ",";
RunOnMainThread is actually a custom function I wrote, so for simplicity sake, here is the full implementation:
private Queue<Task> _mainThreadQueue = new Queue<Task>();
private void Update()
{
lock(_mainThreadQueue)
{
while(_mainThreadQueue.Count > 0)
{
_mainThreadQueue.Dequeue().RunSynchronously();
}
}
}
private Task<T> RunOnMainThread<T>(Task<T> task)
{
lock(_mainThreadQueue)
{
_mainThreadQueue.Enqueue(task);
}
return task;
}
Step by step here, what are all these keywords? RunOnMainThread is a function that takes a Task, that Task is then added to a Queue. Update is a Unity event function that runs at pre-determined intervals, so every time Update runs, it will look at the Queue of Tasks and execute every single one on the current thread. The key here is that since Update is a Unity event, it is guaranteed to be the main thread.
Back to our assignment from the previous block. The keyword “await” is the important part. What happens here is that “await” tells the code to … “wait” for the Task returned by RunOnMainThread to finish. When the Task finishes, the result is automatically pulled out and assigned to our variable. Without the code in Update to execute the Task, this await call would simply block the worker thread forever.
There’s a bit more to it underneath, but at a high level this is everything that needs to happen any time you want to call a Unity API from a worker thread. The await keyword can be used with more than just a Task, and as of C++17, you can do similar things in native code as long as you define an “awaitable interface.”
This whole loop of executing tasks on specific threads is an unfortunate result of interfaces defined as not thread safe, but so many common APIs fall into this category that it may be impossible to ever truly avoid these kinds of call patterns.