Best way to have functions wait for completion across threads?

I’m working on a skill system, and I realize that I need to make skills asynchronous to some degree to permit me to add interruptions in the future. My goal is that when an actor uses a skill, the skill’s function can be paused under specific situations to allow a player to react to it. I thought using C# Tasks would be the solution based on this article and what google seems to tell me, but I can’t quite seem to get it to work.

I’ve just encapsulated my method under a Task, but it doesn’t seem to wait for the completion of certain lines of code, such as a LinecastAll, or Random. Wondering what the best solution is for code on one function to pause until the completion of another function in a separate thread?

Here’s what I have as an example:

  private void useStrike(Unit unit, Unit defender, MAP attackingMap)
    {
        Task.Factory.StartNew(() =>
        {
           // Various non loop code that is waited correctly
            RaycastHit2D[] allHits = Physics2D.LinecastAll(defender.GetPosition(), unit.GetPosition());
           // Various other code, some stuff does not wait ie Random.Range()
        }).Wait();
}

I have async statements later on down the road, which once these preliminary code finishes, should allow it to pause the corresponding function.

Do note that nearly all of Unity’s API can only be called from the main thread. In Unity, async code is also always running on the main thread unless you specifically moved it onto another thread (which you generally should not be doing).

I’m not sure what behaviour expecting is, as all this code should run synchronously. You’re Wait()-ing the task, so it should complete synchronously (though what’s the point of using async) then.

If you want to use actual async code and wait for a method to finish, you need to make it an method that returns Task, and then await the said method.

Right, I had this method as an async Task method originally. However, it’s not even finishing the linecast line I mentioned, let alone the lines further down the road that need to be await-ed to wait to see if any interruptions are made.

To elaborate, this is what the method looked like originally:

private void async Task useStrike(Unit unit, Unit defender, MAP attackingMap)
    {
        Task.Factory.StartNew(() =>
        {
           // Various non loop code that is waited correctly
            RaycastHit2D[] allHits = Physics2D.LinecastAll(defender.GetPosition(), unit.GetPosition());
           // Various other code, some stuff does not wait ie Random.Range()
          await defender.AttackThisUnit(unit, baseDamage);
        }).Wait();
}

However, this method itself is not the function with the important await. The function that calls the above one has the one that can be paused or delayed by user input:

public override async Task UseBase(Unit unit, List<Unit> defenders, System.Action onAttackComplete)
        bool isInterrupted = false;
        bool paused = false;
        
            if (u.InReach(unit))
            {
                Debug.Log(u.characterName + " in reach");
                paused = true;
                await u.onReachCastSpell?.Invoke(unit, () => { 
                    isInterrupted = unit.justCrited;
                    if (isInterrupted)
                    {
                        Debug.Log("INTERRUPTED");
                        unit.justCrited = false;
                        return;
                    }
                }, ReactionTrigger.onReachCastSpell);
            }
       await useStrike();

With the method u.onReachCastSpell?.Invoke being the method that results in pausing of the function.

However, it seems to not wait for other lines before these, and it never seems to do them. For example, it never does any of the preliminary Linecasts, or stuff with random. So it hasn’t even gotten to the functions it needs to actually await for.

From the docs I sent above, it seems to be the case if I understand it right, they show this example function where GetUrlContentLengthAsync won’t finish until getStringTask finishes which it awaits the completion of.

public async Task<int> GetUrlContentLengthAsync()
{
    using var client = new HttpClient();

    Task<string> getStringTask =
        client.GetStringAsync("https://learn.microsoft.com/dotnet");

    DoIndependentWork();

    string contents = await getStringTask;

    return contents.Length;
}

I mean you’re not awaiting useStrike() until after you’ve already awaited another method. So it’s basically going to run top to bottom as normal.

Basically, if it enters the if statement, it awaits a method (that doesn’t seem to do anything asynchronous) and then leaves it. Then it awaits useStrike(), which also does nothing actually asynchronous as you’re just .Wait()ing the method, then continues on.

If you want the execution of one method to await on something else, that needs to happen in said method. For example, pass a Task through that can be awaited.

1 Like

So if the part of the logic that needs to be asynchronous is just this function:

u.onReachCastSpell?.Invoke();

and I should just pass the function call a Task? That will properly ensure that if it gets to this point in the function, the function UseBase will pause the rest of the logic until u.onReachCastSpell?.Invoke(); Don’t need to use Task.Wait() or anything? Just something like:
await u.onReachCastSpell?.Invoke(unit, Task.Factory.StartNew(() =>{ /*Whatever logic I need to wait for user to confirm if they want to interrupt the ability*/}?

More specifically,

public override async Task UseBase(Unit unit, List<Unit> defenders, System.Action onAttackComplete)
{
        // all the stuff I had before
        Task interruption = await u.onReachCastSpell?.Invoke(unit, Task.Factory.StartNew(() =>{ /*As long as onReachCastSpell?.Invoke returns a Task, and is probably async as well?*/} . . . );
}

Something like that, but you you don’t need to use Task.Factory.StartNew() and similar, and you really ought to cut down on the lambda/anonymous functions.

Remember that Task is an object you can return, so you can just call a non-async method that returns a task, and simply await it elsewhere.

For example:

namespace LBG.Testing
{
	using System.Threading.Tasks;
	using UnityEngine;

	public class TestMonoBehaviour : MonoBehaviour
	{
		[ContextMenu("Async Example")]
		public void DoStuff()
		{
			Task someTask = GetSomeTask();
			DoAsyncStuff(someTask);
		}

		public async void DoAsyncStuff(Task task)
		{
			Debug.Log("Before Task");
			await task;
			Debug.Log("After Task");
		}

		private Task GetSomeTask()
		{
			return Task.Delay(1000);
		}
	}
}

Very abstract example but it gets the idea across.

I think I get the idea, In this example, you don’t need to await in DoStuff? If I want to wait for user interaction rather than a delay, it’d just be a .Wait() with the logic for the handling?

If you want to await some external input, then something needs to loop around until said loop is broken.

Eg:

namespace LBG.Testing
{
	using System.Threading;
	using UnityEngine;

	public class TestMonoBehaviour : MonoBehaviour
	{
		private void Awake()
		{
			InputLoopExample();
		}

		private async void InputLoopExample()
		{
			Debug.Log("Before Input loop!");
			await LoopUntilInput(KeyCode.Space, this.destroyCancellationToken);
			Debug.Log("Finished input loop!");
		}

		private async Awaitable LoopUntilInput(KeyCode keyCode, CancellationToken ct)
		{
			while (true)
			{
				if (Input.GetKeyDown(keyCode))
				{
					break;
				}

				await Awaitable.NextFrameAsync(ct);
			}
		}
	}
}

Again, .Wait()-ing a task just makes it execute synchronously. In most cases you do want async code to actually run asynchronously.

2 Likes

You don’t need async code to do this. Just a design that supports it. Basically give each skill a “paused” property that can be set or unset.

Or process skills off of a stack where only the topmost item is processed until finished. And when an existing skill should be interrupted, you just push an “pause” skill on top of the stack and pause simply does nothing.

Thanks so much, I got it working after a good night’s sleep. Who knew coding at 3am probably isn’t the best idea haha

1 Like

That’s true. I always wanted to admittedly learn more about Tasks so I figured it’s both a more robust system, and a good learning opportunity. Thanks for the advice though regardless!

Worth noting the .net Task class is a bit heavy for Unity. Unity now has the Awaitable class that is pooled for less allocations: Unity - Manual: Asynchronous programming with the Awaitable class

There’s also a very popular package for better Unity async support and performance called UniTask: GitHub - Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity.

I tend to use UniTask over Awaitables.

1 Like

On a side note: it feels a little bit weird to me to see tasks and delegates mixed like this:

 
  public async Task UseBase(Action onAttackComplete)

Usually a method would either return a Task or accept an onCompleted delegate, but not both. The only reason for having this sort of structure would be if the UseBase method happens to do something both before and after executing the onAttackComplete delegate. In this situation too, however, the task and delegate could be merged together into just one task that returns another one:

async void Start()
{
    Debug.Log("First part starting.");
    var secondPartAsync = await MultiPartAsync();
    Debug.Log("First part finished.");
    await secondPartAsync;
    Debug.Log("Second part finished.");
}

async Awaitable<Awaitable> MultiPartAsync()
{
    await FirstPart();
    return SecondPart();
}

Awaitable FirstPart() => Awaitable.WaitForSecondsAsync(1f);
Awaitable SecondPart() => Awaitable.WaitForSecondsAsync(1f);

Or, to go in the other direction, it could accept two delegates and return no task:

void Start()
{
    Debug.Log("First part starting.");
    MultiPart
    (
        () => Debug.Log("First part finished."),
        () => Debug.Log("Second part finished.")
    );
}

async void MultiPart(Action onFirstPartComplete, Action onSecondPartComplete)
{
    await FirstPart();
    onFirstPartComplete();
    await SecondPart();
    onSecondPartComplete();
}

Awaitable FirstPart() => Awaitable.WaitForSecondsAsync(1f);
Awaitable SecondPart() => Awaitable.WaitForSecondsAsync(1f);

But maybe your approach does somehow make more sense in your particular context, so take this observation with a grain of salt :slightly_smiling_face: It just feels a bit strange to me to be mixing two different abstractions that can be used to achieve the same end result.