Expose SynchronizationContext and Task extension method from Awaitable

Since unity support Awaitable I think it would be more convenient if unity also expose SynchronizationContext and add extension method for using Task with simpler scheduling function

I made pull request here. It was very simple code addition

So instead of

var obj = await SomeTask();
await Awaitable.MainThreadAsync();
Debug.Log(obj);

We can just

Debug.Log(await SomeTask().OnMainThread());

Let’s not pollute the other thread.

@runner78

And do .NET tasks not switch to the main thread through Unity’s MainThreadSynchronizationContext? I don’t know the behavior when you mix Task and Awaitable.

Technically, awaiting a Task captures the current SynchronizationContext (or TaskScheduler) and returns to it when the task is complete. If you’re not already on the main thread when you await, it will not return to the main thread. Unless you use ConfigureAwait(false), then it will not capture the context, and instead continue on a background thread.

I think ConfigureAwait is the proper terminology to use for the behavior you are implementing.

Common 3rd party and especially network task are not. That was the main reason why we need Awaitable in the first place. As I have mention in that thread and some other threads; firebase need to made it’s own ContinueWithOnMainthread specifically because Task and many async function is not always come back to the unity’s main thread and that always cause error

Or ContinueOnMainThread seems pretty readable to me. ContinueWithOnMainThread sounds like Task.ContinueWith that takes delegate continuation.

I am fine with anything it’s up to unity to decide the name in the end. But I suggest just On... because it shorter

Mixing Tasks and Awaitables is not a good idea. First, this is not an Awaitable Extensions as the name suggest, but Tasks extensions.

Tasks don’t play nice with Unity, because they keep running after the game has stopped in the Unity editor and they need manual cancellation, which is a lot boilerplate code and prone to errors. For this reason Unity created the Awaitable, because it plays nice with Unity’s game loop and ends when the game stops in the editor.

With these extensions the problem is hidden and can cause many problems to any developer that thinks he handles an Awaitable, but in reality he is handling a Task. For example:

public class Example : MonoBehaviour
{

    public Awaitable<int> MyAwaitable;
    
    async void Start()
    {
        var myTask = TestExample();
        MyAwaitable =  myTask.OnEndOfFrame();
        
        var result = await MyAwaitable;
        
        Debug.Log(result);
    }

    private async Task<int> TestExample()
    {
        await Task.Delay(10000);
        Debug.Log("Task Completed");
        return 42;
    }
}

If someone runs this and stops the game before 10 seconds have passed, the “Task Completed” message will be logged in the console anyway.

This creates the problem of having the myAwaitable as a public field for anyone to use. He will think that it behaves as an Awaitable and it honors Unity’s game loop, but in reality it behaves like a task and keeps running independently of Unity.

This extensions class not only has all the negatives of using tasks with Unity and was the reason of creating libraries like UniTask and the Awaitable, but also hides the fact that Tasks are being used by wrapping them with Awaitables that are expected to behave correctly in the Unity ecosystem.

I am quite sure Awaitable was being made for used with Task and any tasklike object. And the point is Task was being made and utilize from dotnet and C# ecosystem for more than 10 years. Almost all of asynchronous object is Task because it was the default async object. And exactly because Awaitable plays nice with Unity’s game loop and ends when the game stops in the editor is the main reason why we should use it to bridge the Task from any other library under unity

What you have try to point out as problem is actually abusing and obscuring your API. Even without my extension method you can simply do it anyway

public class Example : MonoBehaviour
{

    public Awaitable<int> MyAwaitable;
    
    async void Start()
    {
        MyAwaitable =  TestExample(Task.Delay(10000));
       
        var result = await MyAwaitable;
        
        Debug.Log(result);
    }

    private async Awaitable TestExample(Task task)
    {
        await task;
        Debug.Log("Task Completed");
        await Awaitable.
        return 42;
    }
}

No, Awaitable was made to be used in place of Task because of the problems Task has with Unity’s game loop. Both are objects that can be used with the async/await, Awaitable is a type that can be awaited because it implements a getAwaiter method as Task is, it is not a type to be used with other awaitables.

We cannot bridge the Task with Unity, because Task doesn’t care about Unity’s game loop, they are both the same thing, types that can be awaited, one respects Unity’s game loop the other doesn’t, there is no point in using them together.

Because there are many ways to write bad code that doesn’t mean that it should be done. A fundamental principle in OOP is that a type is defined by its behavior, for example both int and float have the same underlying data structure, but they are different because they behave differently.

Having an Awaitable sometimes behave as an Awaitable and other as a Task is bad practice, like having a float with a value let’s say 5 that sometimes behaves like a float and when divided by 2 returns 2.5, and other times acts like an integer and returns 2.

Having extensions that change the Awaitable behavior and writing code as your example are both bad and shouldn’t be done because they change the behavior of Awaitable to be like a Task.

Task cannot be used with Unity because it doesn’t honor Unity’s game loop, there’s no way around it.

I think you miss the point of unity made the class Awaitable with all those static method
BackgroundThreadAsync EndOfFrameAsync FixedUpdateAsync MainThreadAsync NextFrameAsync and so on

Even the description itself specifically mention SomeApiReturningATask as you can see here

as well as an async return type specifically tailored for Unity this agrees with what I’m saying, that there was no async return type that plays well with Unity, so Unity had to make one.

In any case, it’s pretty simple: Task doesn’t play well with Unity and your methods take a Task object and return an Awaitable object. Unless you methods can guarantee that the Task will behave nice with Unity’s game loop, then they fundamentally change the Awaitable behavior to what we had before, async types that are not tailored for Unity’s game loop. Only this time they hide that behavior by making someone believe that because the type is Awaitable is safe to use with Unity’s game loop.

In the Unity example, if the SomeApiReturningTask is implemented in such a way that doesn’t execute code after the game loop has stopped (for example the Task is cancelled) then it is used correctly, if not then it is used incorrectly, but your methods cannot guarantee that for every task object they take as a parameter.

The same way that is the responsibility of the programmer to write a program that cannot be used in the wrong way, for example checks a field that takes an email address if the string has a correct format and if not the program doesn’t crash but reverts state, and is not the responsibility of the user to enter the correctly formatted email address, it is the responsibility of the programmer who writes an API that it cannot be used in a wrong way and not of its users to use it correctly or else it will misbehave.

Unless you methods can guarantee the correct behavior of tasks, that they will stop executing when the game loop stops, they shouldn’t be there in the Awaitable’s API.

I think you misread the sentence

as well as an async return type specifically tailored for Unity

Is the sentence describe the Awaitable that it was being made to be async return type specifically tailored for Unity. And they made it because any other async return type was not being made for unity so they need to made one to bridge the gap

Task doesn’t play well with Unity

And so I use Awaitable which is made for making task play well with unity to convert task into a thing that play well with Unity. Everything my method do is what Awaitable was being made to do. And I think you have too much expectation on Awaitable that it was not made for

As you can see the document was describe a sample

private async Awaitable DoSomethingAsync()
{
   await LoadSceneAsync("SomeScene");
   await SomeApiReturningATask();
   await Awaitable.NextFrameAsync();
   // <...>
}

Which has the exact same behaviour you fear. It return type is async Awaitable and SomeApiReturningATask is completely able to have any behaviour. And so your MyAwaitable can be set to it and exposed in the same way. They have not guarantee that the Task will behave nice with Unity’s game loop to made this function in anyway at all. Which indicate that they don’t care anything about what your concern. It was our responsibility as API consumer

And that responsibility being from knowing that OnMainThread is extension method being made from Awaitable.MainThreadAsync

Only it doesn’t. Anyway we have different interpretations on what Awaitable is, and who is correct only the people who implemented the Awaitable can actually know. Thankfully that is easy to be seen, if your extensions in your pull request become eventually part of the Awaitable API, then you will be correct otherwise I will.

There’s no point continuing, as we have completely different interpretations on what Awaitable is and why it was made.

I think another main issue we have differ from is definition of play well with Unity which yours is very strict and it must guarantee to have so many requirement for it to be call play well with Unity

I still insist that if the sample unity have made are returning type Awaitable while not guarantee anything of your concern it then indicate that it not need to be that strict (has unity ever been as strict as that in their engine code is also arguable)

While my request may not be pulled into unity can be so many reason, such as unity don’t like extension method and they don’t want to have this kind of pattern in their code as much as possible, or anything like that

ps. Maybe we can investigate all the code in UnityCSReference repo to see if there is any Awaitable return from function that not really guarantee to behave so nice with Unity’s game loop

My argument is that if I give you a variable named foo of type Awaitable then you can’t use it correctly because you don’t know its behavior: Will the execution stop when you stop playing in the editor or it will continue like tasks do?

Now, you have an Awaitable type that when awaited has two different behaviors and you don’t know which, unless you dive into the implementation that provides this variable foo.

How about a new type, let’s say that we call it AwaitableTask that can also be awaited and will have all those extension methods, so it can keep the specific functionalities of the Awaitable but now it is clear that it wraps a task and so its behavior is predictable. An example implementation could be like this:

public struct AwaitableTaskMethodBuilder<T>
{
    private static UnityEngine.Awaitable.AwaitableAsyncMethodBuilder<T> s_methodBuilder;

    static AwaitableTaskMethodBuilder() => s_methodBuilder = new UnityEngine.Awaitable.AwaitableAsyncMethodBuilder<T>();

    public static AwaitableTaskMethodBuilder<T> Create() => default;

    public void SetResult(T result) => s_methodBuilder.SetResult(result);

    public void SetException(Exception exception) => s_methodBuilder.SetException(exception);

    public AwaitableTask<T> Task => new(s_methodBuilder.Task);

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine =>
        s_methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine);

    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine =>
        s_methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);

    public void SetStateMachine(IAsyncStateMachine stateMachine) => s_methodBuilder.SetStateMachine(stateMachine);

    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine =>
        s_methodBuilder.Start(ref stateMachine);
}

[AsyncMethodBuilder(typeof(AwaitableTaskMethodBuilder<>))]
public readonly struct AwaitableTask<T>
{
    private readonly Awaitable<T> _awaitable;
    
    public Awaitable<T>.Awaiter GetAwaiter() => AsAwaitable().GetAwaiter();
    internal AwaitableTask(Awaitable<T> awaitable) => _awaitable = awaitable;
    private Awaitable<T> AsAwaitable() => _awaitable ?? throw new InvalidOperationException();
}

Now your methods will have a different signature, for example:

public static async AwaitableTask<T> OnEndOfFrame<T>(this Task<T> task, CancellationToken cancellationToken = default)
{
    var result = await task;
    await UnityEngine.Awaitable.EndOfFrameAsync(cancellationToken);
    return result;
}

They return an AwaitableTask that can also be awaited, and has predictable behavior because it always wraps a Task. For example:

    public AwaitableTask<int> MyAwaitable;

    async void Start()
    {
        Task<int> myTask = TestExample();
        MyAwaitable = myTask.OnEndOfFrame();
          
        var result = await MyAwaitable;

        Debug.Log(result);
    }

My argument is that, it’s unrelate to my extension method at all. Anyone can made that kind of function without my extension method, even the sample from unity itself

private async Awaitable DoSomethingAsync() // it return Awaitable
{
   await LoadSceneAsync("SomeScene");
   await SomeApiReturningATask(); // unity never guarantee this function will stop when you stop playing in the editor or it will continue
   await Awaitable.NextFrameAsync();
   // <...>
}

Anyone can call this DoSomethingAsync() and you argue no one can use it correctly because we don’t know its behavior. Then your argument is against static function of Awaitable itself. Not my proposal to have extension method for it