Search Unity

  1. Unity Asset Manager is now available in public beta. Try it out now and join the conversation here in the forums.
    Dismiss Notice

Feedback Task preemption

Discussion in 'Experimental Scripting Previews' started by Ramobo, Apr 19, 2021.

  1. Ramobo

    Ramobo

    Joined:
    Dec 26, 2018
    Posts:
    212
    Right now, the only advantages that Unity Coroutines have over Tasks are:
    - Can be preempted.
    - Are linked to their `UnityEngine.Object`'s life.
    - First-class engine integration, I guess?

    Tasks can return values, I've never seen one stopping (at different and only some times) for no reason, they have language support, and are overall much nicer. However, pausing and stopping require cooperation, which is a pain in the ass.

    The idea here are `Stop()`, `Pause()`, and `Resume()` `Task` and `Task<T>` extension methods that call into the synchronization context to either not call the task's continuation indefinitely, or just outright drop it (would that cause a memory leak because the runtime still references it?). This can cause deadlocks, yes, but so can stopping coroutines and it's the user's responsibility to avoid them — don't stupidify anything in the name of idiot-proofing it.

    Then, what's missing is automatic linking of tasks to their `UnityEngine.Object` lifetimes.

    But that's backwards-incompatible!


    I figure that this is possible with reflection (I've analyzed `SynchronizationContext` inputs and the generated state machines with a debugger), but it's quite deep and should be easier at the runtime level. The latter should also be possible with IL weaving to add something equivalent to UniTask's UnityAsync's `.ConfigureAwait(this)`.
     

    Attached Files:

    Last edited: Apr 20, 2021
  2. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    The concept of Task and Coroutines is quite different.
    Coroutines run on mainthread Task run on an another Thread.
    What kind of scenario would Task with lifecycle support require that would not be possible with Coroutines?
    Task would be the wrong solution for timers, since the overhead would be greater than the benefit. From inside the Task you can't access Mainthread-only APIs.
     
  3. JesOb

    JesOb

    Joined:
    Sep 3, 2012
    Posts:
    1,109

    You somewhat wrong, Coroutines is only for MainThread, Tasks is not. Tasks can run on any thread. By default task is only for async programming, but if you wish they can be for parallel programming.

    C# dont have good default implementation of Synchronisation context and dont have async model fro console apps, only parralel one. Only UI application make SyncContext for UI, so oy can work async in UI.

    Because of all this in Unity by default tasks is not good and create many garbage. So cysharp created UniTask https://github.com/Cysharp/UniTask, version of task that work correct in Unity and almost dont create garbage
     
  4. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    That's what I said (roughly). Task.Run() run on a Thread from the ThreadPool, also custom async/await. Some async .NET API run on the MainThread when waiting for external results (webrequests, fileoperations).
    I don't know exactly how to implement this for my own task, but it would actually be an interesting alternative to wait for jobs.
    Code (CSharp):
    1. await jobHandle.ValueTask;
    Unity has his own SynchronisationContext.
    .NET core /standard 2.1 has the ValueTask to reduce GC.
     
  5. JesOb

    JesOb

    Joined:
    Sep 3, 2012
    Posts:
    1,109
    Custom async/await will not run on another thread automatically :) The will run where you what they to run actually.

    Basically I what to say that Tasks is not about parallel programming they about async programming and async await will not throw you anywhere, they just wait and everything else depends on SyncContext and others.

    So when we speak about task we speak about async programming model not about multithreading or threads at all.

    UniTask is good implementation of this paradigm for Unity. It works only in main thread until you Explicitly ask to continue on thread pool or in some custom thread, main thread. So by default you dont have any parallel programming, dont need to worry about it, run on MainThread and all Unity Api accessible in async methods.

    With right SyncContext default .Net Task will run the same but it alloc in GC, ValueTask dont allocate only on fast return result. UniTask almost never allocate which is what wee need in Unity :)

    returning to OP question with task link to lifetime of GameObject done like so:

    Code (CSharp):
    1. // .WithCancellation enables Cancel, GetCancellationTokenOnDestroy synchornizes with lifetime of GameObject
    2.     var asset2 = await Resources.LoadAsync<TextAsset>("bar").WithCancellation(this.GetCancellationTokenOnDestroy());
    3.  
     
  6. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    When I tried it in the past in a console application, async tasks always ran on a different thread.
    And that was actually always the case in Unity so far, in an async task method that I was waiting for was always on a different thread. So far i know, Unity's SyncContext guarantees alway return to the main thread after the await, which is not guaranteed in the default behavior, then the code after the await can be a different thread than the code before the await.
     
  7. JesOb

    JesOb

    Joined:
    Sep 3, 2012
    Posts:
    1,109
    You right.
    Default Task behavior very weird, but it is just behavior of default SynchrinosationContext not behavior of Task itself.
    This default behavior is semantically wrong because you write async but get parallel which is not your intent most of the time.

    I wrote: "it run where you want it to run", because this is in your control and can be defined as you wish and this is not how Tasks (async/await) is working overall but just weird defaults :)

    And once again I Love UnityTask because it make semantic of async work as expected all the time by default - be async and never be parallel. To go into parallel world you need to wite:

    Code (CSharp):
    1. // Multithreading, run on ThreadPool under this code
    2.     await UniTask.SwitchToThreadPool();
    3.  
    4.     /* work on ThreadPool */
    5.  
    6.     // return to MainThread(same as `ObserveOnMainThread` in UniRx)
    7.     await UniTask.SwitchToMainThread();