Since newer version of unity introduce Awaitable class that can both using with await and StartCoroutine but also call call it directly (but with warning CS4014)
I wonder what difference and what will happen if we call that function directly as fire and forget event. Compare with using StartCoroutine to wrap it
Is it just that StartCoroutine will stop that Awaitable function if component is disabled?
Please provide a code sample. One for each case you have in mind. And specifically the warning. Because right now I only have a vague understanding of what you mean or what CS4014 is (presumably “call not awaited”), and I’ve actually used Awaitable, await and coroutines in recent days.
But in update we don’t use async/await. We want to just fire and forget
void Update()
{
if(Input.GetMouseButtonDown(0))
DoSomethingAsync(); // This cause `CS4014`
if(Input.GetMouseButtonDown(1))
StartCoRoutine(DoSomethingAsync()); // What exactly difference to above?
}
TIL that Awaitable implements IEnumerator, and can be used as a coroutine. The difference is pretty vast though. Regular async code is a core C# thing, while coroutines are a Unity thing.
I guess the difference is that, when used as a coroutine, it’s lifetime should be tied to the game object’s lifetime. It, hopefully, follows all the same rules as other coroutines.
Otherwise when just called normally, it’s lifetime isn’t tied to a game object. It should follow the rules of regular async code, just that Unity ensures async methods are run on the main thread alongside everything else.
That said, if you don’t intend to await an async method, it should just be async void.
In the CS4014 case, the method is called synchronously not asynchronously, like any other method it will run to completion before the next line executes, with the await keyword in front, the method returns when it encounters an await and other code executes until the await expression inside the method completes.
In the second case, because the Awaitable implements the IEnumerator, it executes like a coroutine + what spiney199 said.
I think you try not to understand the word fire and forget that I don’t want to stall there in update. I just want to fire the async function and let it run free. I want to ask just what it’s behaviour will be
Synchronous means that the method will continue executing before the Awaitable finishes executing, it will not wait for the Awaitable to do whatever it needs to do before executing the next lines after calling the method, that’s what the CS4014 means.
Also, it won’t run on another thread, Awaitables all run on the main thread, unless the Awaitable.BackgroundThreadAsync is used.
This is just a compatibility feature of Awaitable, to be able to use async Awaitable methods inside coroutines. Nothing special happens if you use StartCoroutine with Awaitable, it’ll just waste time waiting.
You can see in the source code that it just returns null until the Awaitable is complete, i.e. checking every frame:
Though, there’s one important point and why I think CS4014 is useful: Discarding a Awaitable or Task will swallow all exceptions. If the async code inside aborts due to an exception, no error will be logged and it can be very confusing to debug.
Using StartCoroutine will handle the exception and properly log it. But this is really a legacy workaround, I use this extension method instead:
/// <summary>
/// Await the Awaitable, in case you don't want to wait for it to complete
/// but still want exceptions to be logged.
/// </summary>
public async static void Await(this Awaitable awaitable)
{
try {
await awaitable;
} catch (OperationCanceledException) {
// Ignore exceptions from cancellation
}
}
If you do want to ignore exceptions, you can use a discard expression to make the warning go away:
I can confirm this to be the case. StartCoroutine does not automatically tie the Awaitable to the lifetime of the MonoBehaviour.
using UnityEngine;
public class Test : MonoBehaviour
{
IEnumerator Start()
{
StartCoroutine(ExampleAsync());
yield return new WaitForSeconds(2f);
// This does not cause the awaitable to be cancelled:
Destroy(this);
}
static async Awaitable ExampleAsync()
{
for(int i = 1; i <= 5; i++)
{
await Awaitable.WaitForSecondsAsync(1f);
Debug.Log(i);
}
Debug.Log("Awaitable finished.");
}
}
using System.Threading;
using UnityEngine;
public class Test2 : MonoBehaviour
{
async void Start()
{
var awaitable = ExampleAsync(destroyCancellationToken);
await Awaitable.WaitForSecondsAsync(2f);
// This does cause the awaitable to be cancelled:
Destroy(this);
await awaitable;
}
static async Awaitable ExampleAsync(CancellationToken cancellationToken)
{
for(int i = 1; i <= 5; i++)
{
await Awaitable.WaitForSecondsAsync(1f, cancellationToken);
Debug.Log(i);
}
Debug.Log("Awaitable finished.");
}
}
Edit: Calling StopCoroutine manually doesn’t cancel an Awaitable that was started using StartCoroutine either:
IEnumerator Start()
{
var coroutine = StartCoroutine(ExampleAsync());
yield return new WaitForSeconds(2f);
// This does nothing:
StopCoroutine(coroutine);
}
I haven’t had much time to look into Awaitables in Unity yet so I’m curious is this is considered good practice (specifically when compared to UniTask and the use of returning UniTaskVoid for fire-and-forget stuff).
The only reason for returning an Awaitable (or a Task for that matter), is to give the callers of the method the ability to track when the asynchronous operation completes, and to be able to know whether it succeeded, failed or was cancelled.
If you never intend to make use of this ability, there’s zero benefit to returning an Awaitable, and all you’re doing is making it more difficult to use the method, and introducing a risk of error hiding (the Awaitable will swallow all exceptions, so all callers need to make sure to pull them out out and rethrow/log them).
(That being said, if you go to any C# community and point this out to them, many will furiously disagree, because they’ve read a blog post from 2013 by Stephen Cleary that had a subtitle called “Avoid Async Void”, which they’ve adapted as the one and only holy unquestionable truth.)
To give a simple practical example, I would never make this method return an Awaitable, because it would be very counter-intuitive to provide users of the API the ability to track the completion of an analytics event inside the method, that has nothing to do with the main functionality of the method, but just happens to be asynchronous in nature:
// Instantly closes this panel
public async void Close()
{
Destroy(this);
await SendAnalyticsEventAsync("Close");
}
Whether a method returns an Awaitable or void is part of the API of the method, and has great implications on how it is used. If you return an Awaitable, you are in most cases strongly implying that callers of the method should await it.
Well I was asking because specifically in UniTask you are implicitly creating a normal Task if you don’t return a UniTask or UniTaskVoid. From what I understand that has to do with how UniTask manages them internally. I just wanted to be sure there weren’t any weird things like that in Unity.
Yeah, there are no such limitations with Unity’s built-in synchronization context. You can mix and match async void, Task, Awaitable and your own Task-like objects freely without any problems.