I am in the process of converting some stuff that i used to do in Coroutines to Async Methods. In general i really like the workflow but there are still some things that i cant get my head around.
In this example i am trying to create an awaitable sequence. No problem with Coroutines - but the Async implementation just runs all Methods in parallel. However if i skip the loop and just write all await calls one after the other, it works.
Is there a better way to do it and why is it not working in the first place.
CODE:
async void Awake()
{
StartCoroutine(EnumeratorSequence(TestCoroutine(), TestCoroutine(), TestCoroutine()));
await TaskSequence(TestTask(), TestTask(), TestTask());
}
public IEnumerator EnumeratorSequence(params IEnumerator[] enumerators)
{
foreach (IEnumerator _e in enumerators)
{
yield return StartCoroutine(_e);
}
}
async Task TaskSequence(params Task[] tasks)
{
foreach (Task _t in tasks)
{
await _t;
}
return;
}
IEnumerator TestCoroutine()
{
Debug.Log("StartCoroutine");
yield return new WaitForSeconds(1);
Debug.Log("EndCoroutine");
}
async Task TestTask()
{
Debug.Log("StartTask");
await Task.Delay(1000);
Debug.Log("EndTask");
}
Well, sure that’s how tasks work unfortunately. Note the main difference here is that when you call a generator method (that returns IEnumerator) the iterator that is returned has not “started” yet. Tasks on the other hand are directly started and run up to their first await. So this line:
await TaskSequence(TestTask(), TestTask(), TestTask());
Calls your TestTask which is run up to its first await where it actually returns the promise “Task” object which you pass to your TaskSequence. Though you essentially kick off all 3 of your tasks up to their first await, so yes, they do run in parallel. What you would have to do is wrapping each task in some sort of a closure which would call the actual task method when it should be started. So you could change your list to Func<Task>
and pass this instead (untested):
await TaskSequence(TestTask, TestTask, TestTask);
of maybe
await TaskSequence(()=>TestTask(), ()=>TestTask(), ()=>TestTask());
if you need arguments. Inside your sequance you would have to call the delegate to start the task and actually get a Task “promise” object back that you can await.
The two approaches (iterator blocks and Tasks) work quite different under the hood. So it’s difficult to get the same behaviour. Technically you could probably create a special “Task” that you await at the beginning on the task so it actually waits for some kind of condition to actually start the rest. Though I think this would be quite messy. Is there a reason you want to switch to async / await? Note that Unity now supports nested IEnumerators. So there’s no need to call StartCoroutine to run a nested coroutine. It actually has several advantages.
Ah thats good to know. Thanks for the indepth explanation. Theres no specific reason, i just read this article
and after some playing around, liked how the code flows compared to Coroutines. Are there any real benefits over using Coroutines that i am not aware of ?
Well, one of the main advantages of Tasks is that you can have return values
Though it’s just a minor point. You can use callbacks with coroutines. The syntax is not as slick, but does work fine.