Search Unity

How to wait for a function with a task to be completed before continuing

Discussion in 'Scripting' started by swifter14, Feb 7, 2019.

  1. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    I'm asking how to wait until a function with a task inside is done, before continuing

    Code (CSharp):
    1. public void A()
    2. {
    3. Debug.Log("before")
    4. CopyInfoFromDB();
    5. Debug.Log("after")
    6. }
    7.  
    8. public void CopyInfoFromDB()
    9. {
    10.           FirebaseDatabase.DefaultInstance.GetReference(path)
    11.  
    12.              .GetValueAsync().ContinueWith(task =>
    13.              {
    14.                  if (task.IsFaulted||task.IsCanceled)
    15. )
    16.                  {
    17.                      Debug.Log("failed");
    18.                      return;
    19.                  }
    20.             name = ...// initializinglocal varibles from Task.result
    21.             });
    22. }
    I want it to wait for CopyInfoFromDB to be completed before printing "after". How should I write function A differently? (I can't return a task from CopyInfoFromDB since I deal with Task.result inside that function and don't need to return anything)
     
    Last edited: Feb 7, 2019
  2. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    Where you are checking whether it failed:
    Code (csharp):
    1.  
    2. Debug.Log(task.IsFaulted ? "failed" : "after");
    3.  
     
  3. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    How is it relevant to my question? If it fails it ends the task, otherwise I use task.Result and export some data from there
     
    EpicWolffe likes this.
  4. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Either pass a callback as an additional argument to your method, or add an event to the class that contains the method.

    If the firebase api is based on later versions of Unity and the runtime and utilizes tasks, you should be able to await the task using the 'await' keyword. I've never used firebase though, and I don't know whether they just have similarly named methods, or if they're actually using tasks.
     
  5. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Thanks for your answer, it seems like I can add AsyncCallback. My question is about Unity, not Firebase, the code is C# so I have await. The question is where to use it in my code so it won't print "after"
     
  6. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    Basically what you are trying to say is that you want a synchronous function to sync to an asynchronous function.

    You need to be careful with how you write these types of methods cause if you start to use asyncs and awaits frivolously you can end up with deadlocks. Among the simplest and best practices to do is to make the entire inside method that is using an async behave asynchronously. My team uses (an in-house version of) the Promise API to do most the heavy lifting.

    A promise is for all intents and purposes an event system that is decoupled from both the issuer and the listeners. you create a promise which is initalized in a pending state. when a promise is in a resolved state (i.e. successful) all handlers attached to the promise via IPromise.Then() fire off. If a promise is in a rejected state all the IPromise.Catch() fire off. You can make a chain of Then/Catch making everything in that chain run in sequence. Plus if you add a Then/Catch handler to a promise that had already resolved/rejected, the respective added handler fires immediately (this is useful in that your handlers don't need to know the context of when the promise was issued).


    Code (CSharp):
    1. public IPromise WaitForDBInfo()
    2. {
    3.     var promise = new Promise();
    4.     myFirebaseReference.getValueAsync().ContinueWith(
    5.         task =>{
    6.         if (task.IsFaulted||task.IsCanceled)
    7.         {
    8.             promise.Reject(new Exception("failed");
    9.             return;
    10.         }
    11.         //initalze data
    12.  
    13.         promise.Resolve();
    14.     });
    15.  
    16. return promise;
    17. }
    its very similar to your CopyInfoFromDB() method, but here we return an IPromise handle which we can bind callback handler code to. I also renamed the method so that it can read better in A()...

    then your A() would wrap the after call inside a promise handler like so...
    Code (CSharp):
    1. public void A()
    2. {
    3. Debug.Log("before");
    4. WaitForDBInfo()
    5.     .Then(()=>Debug.Log("after")) // waits till WaitForDBInfo() finishes succesfully before it runs the action inside Then.
    6.     .Catch(Debug.LogException); //If the Promise was rejected or threw an exception we catch it and display to the console using the Debug class
    7.     .Done();// since .Then() and .Catch() conatenates a promise chain we tell that we are done adding to the chain so the promise can resolve.
    8. }
    basically I've made all the synchronous code written after the async methods which wants to wait on that method (e.g. the "after") asynchronous by wrapping it in a "Then handler".

    so now what happens in A() is that "before" prints to console as usual, and the DB request is sent out. Then the function completes, not needing to wait on the result to come back before returning. when the result does finally comeback the firebase response is then processed, if the response was successful the data is then initialized, and "After" prints to console. however if the response failed then the data is not initialized,"after" is NOT printed, and instead the exception "failed" is printed to console.

    A() remains synchronized as well as anything calling A() and expecting it to behave synchronously. its not waiting on Firebase before returning control back the the caller. the "after" stuff is simply run as a callback once the inital promise is resolved.
     
  7. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Hey, Joshua thanks for the detailed answer! I'm still processing it, but I have a question.
    "you want a synchronous function to sync to an asynchronous function"
    Would it help if I change function A to be asynchronous. Maybe it'd help if I explain the function-
    I have a "BUY" button, it calls function A , function A checks with CopyInfoFromDB() that there is enough money in the Database (using task). . and then completes function A and buys the objects if found enough money in DB (that's why I needed to wait)
     
  8. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Well, it depends... The API of Firebase could be relevant, simply because if it already uses tasks (which it apparently does), you can stop working around it and just make your method await it all the way up, and return either a task or make your method the "root" of an async call stack (async void, which is not awaitable at the call site).

    There are basically two different ways to handle each new method / task.

    The first is to simply return a task and make the caller decide what to do with it. This can be imagined as if you just pass a task around, without triggering its execution at any point and leave it up to the caller. This is mainly useful, when you want to prepare the tasks for later use.

    The second is to just await the task at any level, so that you're forced (or rather encouraged, because not awaiting it would just generate a warning) to run this asynchronously. This is mainly useful to make things happen upon calling an async method, and/or if you need to await something before a method returns another awaitable result.

    Let's first look at the first case:
    Code (CSharp):
    1. private string _name = null;
    2. private async void Awake()
    3. {
    4.     Debug.Log("before");
    5.  
    6.     await CopyInfoFromDB_ReturnTask();
    7.  
    8.     Debug.Log("after");
    9.     Debug.Log(_name);
    10. }
    11.  
    12. public Task CopyInfoFromDB_ReturnTask() // note the return type, not async
    13. {
    14.     // only creates and returns the task, just as if it was a normal return type, hence no "async" in front of the method's return type
    15.     return FirebaseDatabase.DefaultInstance.GetReference(path).GetValueAsync().ContinueWith(task =>
    16.     {
    17.         if (task.IsFaulted || task.IsCanceled)
    18.         {
    19.             Debug.Log("Inner task was " + (task.IsFaulted ? "faulted." : "cancelled."));
    20.             return;
    21.         }
    22.  
    23.         // do initializations
    24.         // the results could as well be returned within a generic Task<T> in the
    25.         // continuation task, and bubble up through your method to the call site
    26.         _name = "Name that's in the database.";
    27.         return; // not necessary, no value to return since we do not use a generic task
    28.     });
    29. }
    As you can see, the task will be created by your method, but it doesn't want to do anything special after it's been completed... it only serves for the purpose of constructing the chain of tasks.
    On the other hand, Awake decides to immediately await the returned task... Awake could also just cache it and pass it somewhere else where it's then raised at any time, but you're free to handle this in any way you want.

    I assume though, that GetValueAsync of the FirebaseDatabasse is marked 'async', i.e. it's meant to be awaited, which leads us to the next solution:

    Code (CSharp):
    1. private string _name = null;
    2. private async void Awake()
    3. {
    4.     Debug.Log("before");
    5.  
    6.     await CopyInfoFromDB_Awaitable();
    7.  
    8.     Debug.Log("after");
    9.     Debug.Log(_name);
    10. }
    11.  
    12. public async Task CopyInfoFromDB_Awaitable() // note: an additional 'async' keyword
    13. {
    14.     // this requires 'async' in in front of the method's return type, as we want to await it within this method
    15.     await FirebaseDatabase.DefaultInstance.GetReference(path).GetValueAsync().ContinueWith(task =>
    16.     {
    17.         if (task.IsFaulted || task.IsCanceled)
    18.         {
    19.             Debug.Log("Inner task was " + (task.IsFaulted ? "faulted." : "cancelled."));
    20.             return;
    21.         }
    22.  
    23.         // do initializations
    24.         // the results could as well be returned within a generic Task<T> in the
    25.         // continuation task, and bubble up through your method to the call site
    26.         _name = "Name that's in the database.";
    27.         return; // not necessary, no value to return, since we do not use a geneic task
    28.     });
    29.  
    30.     // anything that you put here will be run once the awaiting above has finished
    31. }
    As you can see, Awake awaits the version of CopyInfoFromDB again, but this time, it's marked async and the implementation of that method itself awaits another task internally, which happens to be your async database access.

    This is just to get you started.
    If you read the commented parts carefully, you'll notice that you can change the way the tasks communicate with their callers.
    Instead of having the continuation fill the instance fields, you could switch to a return type of a generic task, and return the info that you've just read from the database, allowing the caller to pre-process everything.
    You could also return an indicator that tells the caller whether the task was successfull or not, making it easier to evaluate the current state for subsequent steps.

    That's all up to you.
     
    Last edited: Feb 8, 2019
  9. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Thanks for your answer you are great!! I'm going to test it and write back when I'm done
     
  10. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Suddoha so I followed your suggestion, it looks good, the only problem with that is that I must return a Task, so for example if I have a condition before the task that might be false- the function won't create a task at all, and then I'd have nothing to return. So maybe I should return some fake empty task to be able to use your code and skip the waiting? And the same thing when I use "try" for my task and it fails so I have to catch it and create an empty task to return because I have to return a task

    Code (CSharp):
    1. public async Task CopyInfoFromDB_Awaitable() // note: an additional 'async' keyword
    2. {  if (name.Equals("Tom"){
    3.     // this requires 'async' in in front of the method's return type, as we want to await it within this method
    4.     await FirebaseDatabase.DefaultInstance.GetReference(path).GetValueAsync().ContinueWith(task =>
    5.     {
    6.         if (task.IsFaulted || task.IsCanceled)
    7.         {
    8.             Debug.Log("Inner task was " + (task.IsFaulted ? "faulted." : "cancelled."));
    9.             return;
    10.         }
    11.         // do initializations
    12.         // the results could as well be returned within a generic Task<T> in the
    13.         // continuation task, and bubble up through your method to the call site
    14.         _name = "Name that's in the database.";
    15.         return; // not necessary, no value to return, since we do not use a geneic task
    16.     });
    17.     // nothing to return....
    18. }}
     
  11. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    That only applies to one of the approaches, you have to distinguish the two scenarios I've demonstrated.

    The first one (not marked async) requires you to return an actual task object, whereas the second one (marked async) requires you to return anything that matches the task's result, except for the non-generic Task.

    That is, the return type of
    async Task
    only requires your async method to return, which happens implicity if you do not explicitly return (just as if you had a void method). However, when you use
    async Task<T>
    , you must either await another task of type Task<T> (or anything compatible to that), or a T if you do not await anything anymore.

    Code (csharp):
    1. Task Example1() { ...} // a normal method that returns a Task from its body (or anything that's assignable to Task)
    2. Task<T> Example2() { ... } // a normal method that returns Task<T> from its body (or anything that's assignable to Task<T>)
    3. async Task Example1Async() { ... } // an "async" method that simply returns from its body
    4. async Task<T> Example2Async() { ... } // an "async" method that must return a T (or anything that's assignable to T)
    The snippet you've chosen to work with uses 'async Task' as in Example1Async, so you should be able to return implicity or explicity. You're correct when you use simply return Tasks from non-async methods, as in Example1 - this one requires a Task to be returned. For cases in which you have no tasks to create and return, Task.CompletedTask was introduced with .Net 4.6. You can simply return that one instead.
     
  12. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Hey Suddoha thanks for that, the reason why I chose the first approach is because I also use CallAsync task and I noticed that Firebase has this example-
    Code (CSharp):
    1. private Task<string> addMessage(string text) {
    2.   // Create the arguments to the callable function.
    3.   var data = new Dictionary<string, object>();
    4.   data["text"] = text;
    5.   data["push"] = true;
    6.  
    7.   // Call the function and extract the operation from the result.
    8.   var function = functions.GetHttpsCallable("addMessage");
    9.   return function.CallAsync(data).ContinueWith((task) => {
    10.     return (string) task.Result.Data;
    11.   });
    12. }
    But you are right- I need to use the second approach since I don't want to return anything back- the only problem is that I'll have to use "await" as you said -
    await function.CallAsync(data).ContinueWith((task) =>.......
    But then I'll get
    "Task does not contain a definition for 'GetAwaiter' and no extension method 'GetAwaiter' accepting a first argument of type 'Task' could be found (are you missing a using directive or an assembly reference?)"

    I noticed that I can solve everything with coroutines (since I don't have to return values anyway) but I read that coroutines are not good for try and catch... I'm confused...
     
  13. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    What's your project's player configuration? Have you included the required namespaces? Are you trying to use dynamics?

    *Edit Got it now, removed further explanations.

    Can you post the complete example that causes trouble?
     
    Last edited: Feb 12, 2019
  14. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Hey. What do you mean to include required namespaces? I get that error ""Task does not contain a definition for 'GetAwaiter'..." only with Callable tasks, here's my full example-
    Code (CSharp):
    1.  private async Task BuyItem(string text)
    2.     {
    3.         // Create the arguments to the callable function.
    4.         var data = new Dictionary<string, object>();
    5.         data["text"] = text;
    6.         data["push"] = true;
    7.  
    8.         // Call the function and extract the operation from the result.
    9.         var function = functions.GetHttpsCallable("buyItem");
    10.         await function.CallAsync(data).ContinueWith((task) => {
    11.             if (task.IsFaulted || task.IsCanceled)
    12.             {
    13.                 Debug.Log("");
    14.                 return;
    15.             }
    16.  
    17.             int money = (int)task.Result.Data;
    18.             userMoney = money;
    19.             return;
    20.         });
    21.     }
    As you can see in the picture, the minute I add "await"- everything becomes red.
    Note that it happens only with this type of Callable tasks (CallAsync),
    I have no problems adding "await" with regular tasks like the examples you gave me with GetValueAsync
     
  15. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    That's weird. I checked their API and it definitely returns a task.

    I searched a bit via google and it's a common issue when you attempt to use tasks between .NET 4 and .NET 4.6, as the GetAwaiter stuff was introduced with 4.6. Additional installations are required to make it work prior to that.

    Now, the thing is that Unity probably does not use anything in between (can't tell for sure, I'm mainly sticking to .NET 3.5 because I have to for the current projects) and it used to show 4.x in the highest version that I have ever used - which basically says nothing but well, 4.0 or higher or another weirdly crafted "special" version.

    It also wouldn't make a hell lot of sense that it works with all tasks, except for those, unless you're using a rare mixture of assemblies that've been compiled against different versions.

    I'd definitely check that you're using the most recent stable releases of firebase, I have currently no other ideas and don't have lots of time to test things. However, you could upload a minimal project that re-produces the behaviour on your side, and upload it for me / others who want to fiddle with it.
     
  16. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
  17. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    The errors wouldn't surprise me if you really use the .NET 3.5 version, as already mentioned earlier.

    Try to follow the 'known issues' section here, which covers the compatibility issues when using projects with another configuration. They talk about enabling and disabling the relevant assemblies for the corresponding versions. Not sure which implications that'll have on the usage and the API in general...
     
    Last edited: Feb 13, 2019
  18. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    You were right, I updated .net and I don't get this error anymore.

    I started to use your code, but I noticed that there's a problem with nested awaits- it won't won't wait for the function to be completed. For example, running the following code will print

    -before
    -first task completed
    -Starting SendInfoToDatabase
    -after
    -name
    -Finishing SendInfoToDatabase

    But I want it to be:

    -before
    -first task completed
    -Starting SendInfoToDatabase
    -Finishing SendInfoToDatabase
    -after
    -name



    Code (CSharp):
    1. private string _name = null;
    2. private async void Awake()
    3. {
    4.     Debug.Log("before");
    5.     await CopyInfoFromDB_Awaitable();
    6.     Debug.Log("after");
    7.     Debug.Log(_name);
    8. }
    9. public async Task CopyInfoFromDB_Awaitable()
    10. {
    11.      await FirebaseDatabase.DefaultInstance.GetReference(path).GetValueAsync().ContinueWith(task =>
    12.     {
    13.         if (task.IsFaulted || task.IsCanceled)
    14.         {
    15.             Debug.Log("Inner task was " + (task.IsFaulted ? "faulted." : "cancelled."));
    16.             return;
    17.         }
    18.         // do initializations
    19.           _name = "Name that's in the database.";
    20.            Debug.Log("first task completed");
    21.        await SendInfoToDatabase();
    22.         return;
    23.     });
    24. }
    25. public async Task SendInfoToDatabase()
    26. {
    27.   Debug.Log("Starting SendInfoToDatabase");
    28.    await reference.SetValueAsync("done");
    29.    Debug.Log("Finishing SendInfoToDatabase");
    30. }
     
  19. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Well, now you've used another feature that you didn't use before, perhaps that wasn't your intention,

    First things first, was 'await SendInfoToDatabase' meant to be placed outside the continuation's lambda? In that case, you'd have your desired behaviour: The async query will be awaited and when it's finished, the rest of CopyInfo... will be run. That is, anything that follows can either be run synchronously, or it can be awaited.

    So ONE way to fix your issue the following (it's not perfect, as you obviously want to check the task's result):
    Code (CSharp):
    1. await FirebaseDatabase.DefaultInstance.GetReference(path).GetValueAsync().ContinueWith(task =>
    2. {
    3.     if (task.IsFaulted || task.IsCanceled)
    4.     {
    5.         Debug.Log("Inner task was " + (task.IsFaulted ? "faulted." : "cancelled."));
    6.         return;
    7.     }
    8.     // do initializations
    9.     _name = "Name that's in the database.";
    10.     Debug.Log("first task completed");
    11.     return;
    12. });
    13.  
    14. await SendInfoToDatabase();
    The second awaitable is now on the same level as the first awaitable, and they will be scheduled one after the other correctly, and both have to complete (or throw, in exceptional cases) before control is returned to the caller.

    As you may notice, the second one will always execute at the moment - you have to communicate the tasks and the queries result for further decision making.

    There are many ways to do that, here are a few of them:

    1) you could first get the task, save it locally in a variable of the tasks's type, then await it, and before you continue with the next awaitable, check its result - this might look ugly though

    2) You could use the actual result of the task and work with that after the task's completion, or use your own result that you specify as the return value of the continuation's function.

    3) You could save the result either in an instance variable of the wrapping type, or in a locally captured variable that you write to from within the continuation - that's just a lambda / delegate which captures everything that is used. You'd then work with those variables (or fields respectively) in order to determine whether you want to continue with SendInfoToDatabase. That's basically similar to 2), it simply works with variables / fields instead of returns values.

    4) Throw an exception that you handle on a level that's meant to care about that - you should only use this when it's really an exceptional behaviour (here's an excellent but short blog entry about basic exception "categories" - absolutely worth reading if you haven't yet).

    Let's head back to your latest snippet, and its execution behaviour:

    All the snippets prior to your latest version have a simple, "normal" continuation which is also solely meant to execute synchronously.
    That's sorta like a normal method, I mean, you could say it works like "when this task completes, do this sequence of commands and then return to the awaiting caller, no more async stuff".

    But you've changed the game a little a bit, as you start off another asynchronous call (btw - I'm not sure about later versions, but the continuation's lambda needs the 'async' keyword in order to be compilable, because you're using await inside).

    What this effectively does is, that you schedule a continuation for the task as before, but you also want a part of it to run asynchronously from within the continuation,.

    By default, this executes as follows:
    The outer task runs all of its instructions, when it completes, the continuation is triggered, i.e. it executes. However, if that continuation is marked 'async', the continuation will run synchronously until it hits the first await - it then returns control to the caller (that's the call sit of the continuation), and the outer task is then "complete", bubbles up, if there are more awaitables, those will be awaited, until it finally reaches the root of everything.

    In your particular example, the async firebase query runs, when the task's execution finishes (complete, cancelled, whatever) it triggers the continuation (ContinueWith) and that one runs synchronously - when it hits your asynchronous call to SetValueAsync, it'll start to await it, but then resumes immediately to CopyInfo... There's nothing to execute / await, so again it bubbles up to Awake and continues with that in the same way.

    Meanwhile, your newly started SetValueAsync keeps running detached from the original task, hence the incorrect order (technically it is correct).

    You can fix that, if you specifiy that the inner asynchronous call will be attached to its parent, as documented here (TaskContinuationOptions.AttachToParent).
     
    Last edited: Feb 17, 2019
  20. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Thanks for all that info!!
    Yes, I placed await SendInfoToDatabase inside the lambda because I was waiting for the task to be completed and use task.result, then continue with SendInfoToDatabase.

    Is TaskContinuationOptions.AttachToParent is another solution instead of 1 to 4 examples you gave?
    If so where do I use it? inside CopyInfoFromDB or inside SendInfoToDatabase,

    My main goal is that the functions shouldn't bubble up to parent until it's completely done, and finish all the inner "await" before returning to parent.

    I think I'll have to rewrite all tasks in my code and add "continue with" and TaskContinuationOptions.AttachToParent , because I have a lot of inner calls to different functions, and I want my code to finish the child functions before bubble up, even if there's await in a child task function it should finish with it before going back to a parent.

    I tried this code but still the same problem:

    Code (CSharp):
    1. private string _name = null;
    2. private async void Awake()
    3. {
    4.     Debug.Log("before");
    5.     await CopyInfoFromDB_Awaitable();
    6.     Debug.Log("after");
    7.     Debug.Log(_name);
    8. }
    9. public async Task CopyInfoFromDB_Awaitable()
    10. {
    11.      await FirebaseDatabase.DefaultInstance.GetReference(path).GetValueAsync().ContinueWith(async task =>
    12.     {
    13.         if (task.IsFaulted || task.IsCanceled)
    14.         {
    15.             Debug.Log("Inner task was " + (task.IsFaulted ? "faulted." : "cancelled."));
    16.             return;
    17.         }
    18.         // do initializations
    19.           _name = "Name that's in the database.";
    20.            Debug.Log("first task completed");
    21.        await SendInfoToDatabase();
    22.         return;
    23.     },TaskContinuationOptions.AttachedToParent);
    24. }
    25. public async Task SendInfoToDatabase()
    26. {
    27.   Debug.Log("Starting SendInfoToDatabase");
    28.    await reference.SetValueAsync("done").ContinueWith(task=> Debug.Log("done1") , TaskContinuationOptions.AttachedToParent );
    29.                 Debug.Log("After Done1");
    30.   await reference.SetValueAsync("done").ContinueWith(task=> Debug.Log("done2") , TaskContinuationOptions.AttachedToParent );
    31.   await reference.SetValueAsync("done").ContinueWith(task=> Debug.Log("done3") , TaskContinuationOptions.AttachedToParent );
    32.    Debug.Log("Finishing SendInfoToDatabase");
    33. }
    My code prints:
    -before
    -first task completed
    -Starting SendInfoToDatabase
    -done1
    -After Done1
    -after
    -name
    -done2
    -done3
    -Finishing SendInfoToDatabase
    But I need it to be- (before,first task completed, Starting SendInfoToDatabase,done1,after done1,done2,done3,Finishing SendInfoToDatabase,after, name)
     
    Last edited: Feb 17, 2019
  21. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    I hope you don't mind all the walls of text and code examples, it might be a little messy sometimes.

    You configured the parent task, not the child task. The child task is the awaitable that resides in the scheduled continuation.

    Personally, I'd say that specifying the continuation via ContinueWith is probably most useful when you create a task that is only returned, but not awaited where it's created. It's also suitable as a shorthand writing when you want to have a synchronous continuation (like the firebase examples) or a "fire and forget" async continuation (the unexpected behaviour that you encountered).

    Now let's first get back to your latest solution. If you want to stick to nested tasks, you're good to go with something along these lines

    Code (CSharp):
    1. private async void Awake()
    2. {
    3.     Debug.Log("Parent task: before");
    4.     await Task.Delay(1000).ContinueWith(async task =>
    5.     {
    6.         Debug.Log("Child task: before");
    7.         await Task.Delay(1000).ContinueWith((t) => { /* further continuation */ }, TaskContinuationOptions.AttachedToParent);
    8.         Debug.Log("Child task: after");
    9.     });
    10.     Debug.Log("Parent task: after");
    11. }
    That's one of the short ways to make the parent wait for child tasks, without using any of the task factories IIRC. However, this already starts to look weird and this clutters the code when you have more complex continuations, and there are better ways to achieve the same effect.

    As mentioned earlier, you should attempt to work with the tasks in a slightly different way, save a reference to a task or return an appropriate result that you can work with.

    Here's just one more example that does what you want, too, without nesting tasks inside another and it's much more readable (it's the first approach I mentioned in my previous post, i.e. it first saves a reference to the task).

    Code (CSharp):
    1. public async Task CopyInfoFromDB_Awaitable()
    2. {
    3.     // reference to task only required if you want to check its completion state
    4.     var task = FirebaseDatabase.DefaultInstance.GetReference(_path).GetValueAsync();
    5.     await task;
    6.  
    7.     if (task.IsCanceled || task.IsFaulted)
    8.     {
    9.         Debug.Log("Query task fauled or canceled.");
    10.         return;
    11.     }
    12.  
    13.     _name = "Name from task result";
    14.     await SendInfoToDatabase();
    15. }

    Alternatives
    Now there are multiple ways to make this look nicer. You can schedule a continuation that returns the task itself, which allows to put the first two lines into a single line, like this:

    Code (CSharp):
    1. public async Task CopyInfoFromDB()
    2. {
    3.     // reference to task only required if you want to check its completion state
    4.     var task =  await FirebaseDatabase.DefaultInstance.GetReference(_path).GetValueAsync().ContinueWith(t => t);
    5.  
    6.     if (task.IsCanceled || task.IsFaulted)
    7.     {
    8.         Debug.Log("Query task fauled or canceled.");
    9.         return;
    10.     }
    11.  
    12.     // do your initialization and extract the values
    13.  
    14.     // then await the write operation
    15.     await SendInfoToDatabase();
    16. }
    Note that I used a simple 'Task'. For the ease of use (and in order to omit a cast in the calling code) you'd want to use a continuation that returns the concrete type of the task, in this example a Task<DataSnapshot>.


    You could as well stick to the continuation, handle the state internally and return the DataSnapshot that's contained in GetValueAsync's Task...

    Code (CSharp):
    1. public async Task CopyInfoFromDB()
    2. {
    3.     var snapshot =  await FirebaseDatabase.DefaultInstance.GetReference(_path).GetValueAsync().ContinueWith(t =>
    4.     {
    5.         if (t.IsCanceled || t.IsFaulted)
    6.         {
    7.             return null; // that would indicate there wasn't a result for some reason
    8.         }
    9.  
    10.         return (t as Task<DataSnapshot>).Result;
    11.     });
    12.  
    13.     // do some stuff
    14.  
    15.     await SendInfoToDatabase();
    16. }
    Or, another way, just get rid of the snapshot internally, and only return the pure value (if that's all you need)

    Code (CSharp):
    1. public async Task CopyInfoFromDB_Awaitable()
    2. {
    3.     var extractedValue =  await FirebaseDatabase.DefaultInstance.GetReference(_path).GetValueAsync().ContinueWith(t =>
    4.     {
    5.         if (t.IsCanceled || t.IsFaulted)
    6.         {
    7.             return null; // return something that indicates that the value wasn't available
    8.         }
    9.  
    10.         return (string)(t as Task<DataSnapshot>).Result.Value; // or something, not sure about all the types in the Firebase API
    11.     });
    12.  
    13.     // do what you need to do here
    14.     await SendInfoToDatabase();
    15. }
    As you can see, there are many ways to work with tasks. The difference between the last three examples is that you have different access to the objects that are involded.
    The first can easily grant access to the task's completion result, you can evaluate it before you do any further processing or just return early.

    The second gets rid of the task once the continuation is done, and the result of the task (which is a Task<DataSnapshot in your example) is unwrapped.
    With that approach, you can work on that complete snapshot, and all values contained in it. 'Null' serves as an indicator for any failure - in this case, the code wouldn't really care why it has failed, but only whether values could be retreived or not.

    The last example goes even one step further. It only returns the actual value that you're looking for. The task and its result (the data snapshot) are both handled within the continuation, and the continuation does all the extraction so that the caller only receives a specific value (or an indicator for 'not available' or any type of failure.

    What you end up doing is up to you.
     
    Last edited: Feb 18, 2019
  22. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Hey, thank you for your answer! I'm still amazed by your detailed answers, and very thankful!

    I don't really need nested tasks, the reason why it's written that way is because "await" command is new in my code,
    so before that, I needed a place to initialize data once I knew for sure task is done (inside ContinueWith).
    But, now with "await", I can totally use your suggestion and move out the SendInfoToDatabase() call, and write it this way:

    Code (CSharp):
    1. private string _name = null;
    2. private async void Awake()
    3. {
    4.     Debug.Log("before");
    5.     await CopyInfoFromDB_Awaitable();
    6.     Debug.Log("after");
    7.     Debug.Log(_name);
    8. }
    9. public async Task CopyInfoFromDB_Awaitable()
    10. {
    11.       var task =  await FirebaseDatabase.DefaultInstance.GetReference(path).GetValueAsync().ContinueWith(t =>t);
    12.        if (task.IsFaulted || task.IsCanceled)
    13.         {
    14.             Debug.Log("Inner task was " + (task.IsFaulted ? "faulted." : "cancelled."));
    15.             return;
    16.         }
    17.         // do initializations with task.Result
    18.         await SendInfoToDatabase();
    19.         return;
    20. }
    21. public async Task SendInfoToDatabase()
    22. {
    23.   Debug.Log("Starting SendInfoToDatabase");
    24.    await reference.SetValueAsync("done").ContinueWith(task=> task);
    25.                 Debug.Log("After Done1");
    26.   await reference.SetValueAsync("done").ContinueWith(task=> task);
    27.               Debug.Log("After Done2");
    28.   await reference.SetValueAsync("done").ContinueWith(task=> task);
    29.    Debug.Log("Finishing SendInfoToDatabase");
    30. }
    Is my code correct? So now I don't need AttachedToParent anymore and also no need for await Task.Delay(1000) for Awake() function.

    But now something isn't right because sometimes I get the following output:
    -Before
    -Starting SendInfoToDatabase
    And it doesn't continue anymore with the tasks... I don't know why it stops and doesn't continue to complete the function (I was even waiting 10 minutes), so I started to think that maybe I should use some timeout catcher for await- to release and stop all waiting tasks if a task takes too long.
    So I started by adding my first timeout catcher for where it got stuck (inside SendInfoToDatabase())
    Code (CSharp):
    1.        var task =  reference.SetValueAsync("done").ContinueWith(t=> t);
    2.                     if (await Task.WhenAny(task, Task.Delay(5)) == task)
    3.                     { Debug.Log("Timeout fail"); }
    4.                                  Debug.Log("sendusertodb");
    But after running the code it seemed to be working fine, without catching any timeout problem (it didn't print "Timeout fail"), so something is not right.
     
    Last edited: Feb 19, 2019
    Suddoha likes this.
  23. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Yep, you don't need either of them. Using Task.Delay in order to "ensure" the task will be complete would be a very dirty, unreliable work-around (more likely a hack-around). So yes, it's good that you no longer need to use it.
    And yes, the continuation option is no longer needed either, because the tasks are now set to execute sequentially, i.e. one after the completion of the other.

    It looks correct so far (I cannot test it at the moment).

    Small additional note though:
    Code (csharp):
    1. public async Task SendInfoToDatabase()
    2. {
    3.   Debug.Log("Starting SendInfoToDatabase");
    4.    await reference.SetValueAsync("done").ContinueWith(task=> task);
    5.                 Debug.Log("After Done1");
    6.   await reference.SetValueAsync("done").ContinueWith(task=> task);
    7.               Debug.Log("After Done2");
    8.   await reference.SetValueAsync("done").ContinueWith(task=> task);
    9.    Debug.Log("Finishing SendInfoToDatabase");
    10. }
    You can remove the continuations (ContinueWith) in these three lines, if you don't need to access the task after completion.

    I mean, the sole purpose of the continuation that I added to the other line was to get access to the task at the call site in a more aesthetic way (personal preference, as it looks cleaner to me). I just don't like the two-line version, and prefer the one-line version.

    In these three lines, you're not assigning the result to anything, so you can simply remove it.


    That's something you have to debug now. The first thing you should do is wrapping everything into a try-catch construct. The reason is quite simple: You're dealing with an external service, that your program cannot control completely. Network-issues, DB errors, hardware errors always introduce unpredictable error cases, in other words, excpetional behaviour, that you have to catch, and handle.

    Perhaps that's already it, and the exception might contain enough information to fix the issue.
    If you do not properly catch exceptions that occur in asynchronous methods, you run the risk of missing the exception, as it could be swallowed by the engine - especially when it occurs off the main thread.

    If no exception is thrown, you should definitely attempt to use other firebase operations, or start off with some stubs to track down the issue.
     
    Last edited: Feb 19, 2019
  24. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Thanks, I'm working on it, I do use Try and Catch in my code but it catches nothing, just doesn't continue like in my example, I'll have to debug it. Thanks for your help! too bad I can't rank you here.
     
  25. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Where did you add try-catch? You can try to experiment with Task.Delay and Task.CompletedTasks to see if it generally works.

    Hopefully you'll be able to resolve it soon enough.

    You're welcome, and that's no problem at all. I'm here to help, I do not really care about the likes, post counts etc. :D
     
  26. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Code (CSharp):
    1. public async Task SendInfoToDatabase()
    2. { try
    3. {
    4.   Debug.Log("Starting SendInfoToDatabase");
    5.    await reference.SetValueAsync("done");
    6.                 Debug.Log("After Done1");
    7.   await reference.SetValueAsync("done");
    8.               Debug.Log("After Done2");
    9.   await reference.SetValueAsync("done");
    10.    Debug.Log("Finishing SendInfoToDatabase");}
    11. catch{Debug.Log("error");}
    12. }
    It doesn't happen every time, but sometimes- The SendInfoToDatabase function
    prints "Starting SendInfoToDatabase" and that's it. And because my other DB functions are dependent (with await) for this call to be completed, it's all "stuck".
    When I try to call the function again, it's the same thing, stops at the same place
    But that's also bad to call it again because now I have few calls that "await" for this function to be done, how do I make sure to clear them all?

    I'll try to do what you said. Thanks
     
  27. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Yes, I already followed you up to that point.

    So, if no expection is thrown, the next step is to check the tasks that are used for the async calls.

    await reference.SetValueAsync(...)
    - take the task that's involved. You can check its status again using either a reference to it (as we had done it with the other line of code) or simply add a continuation.

    Code (csharp):
    1. await reference.SetValueAsync("done").ContinueWith(task => // check status)
    (code written in the browser, may contain typos)

    Hopefully this will reveal what's going wrong.
     
  28. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Hey! So far everything works well, my transformation to Tasks and Await is not complete though:
    Previously I used to have:
    Code (CSharp):
    1.  Public IEnumerator FBConnect(){
    2. // ....connecting to Facebook
    3.  while (!FB.IsLoggedIn) {  //while still not connected to facebook
    4.  
    5.         if (timer > waitTimeOut)
    6.         {
    7.             Debug.LogError("Failed to log out within " + waitTimeOut + " seconds");
    8.             yield break;
    9.         }
    10.         timer += Time.deltaTime;
    11.         yield return null;
    12.     }
    13.     Debug.Log("Successfully logged in");
    14.     //a lot of code
    }

    The reason for the while loop is because - I have to wait until I get a response from Facebook that the user is logged in (waiting until the user will approve/reject all of Facebook permissions to allow my app to use Facebook), once the user is done approving, the while loop will be ended and the rest of the code will continue.

    But now I need to translate my code to await and tasks instead of yield and IEnumerator
    , and not sure what the best way to do it .
     
  29. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    I'm not familiar with the social media APIs.

    First thing that needs to be checked is whether or not there's already a part of the APIs that make use of tasks. Could you check that? If that's not the case, you'll need to wrap that with in your own tasks / your own async methods if you really want to use async-await.
     
  30. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Last edited: Mar 31, 2019
  31. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    The question about the API arose because I thought there might be the built-in way to use async-await ith that API. But if you say it doesn't exist, we'll just skip that part.

    For your particular case you'll still need some sort of polling mechanism.
    You could create a task that runs its polling in the background on a different thread (if the API allows to check the status from any other thread).

    You could as well use a TaskCompletionSource that you set complete from a component that checks your login status , either in Update or from a coroutine (the coroutine would be simple helper, an implementation detail that no client of your async method should ever care about).

    Third approach would require some sort of event of the FB SDK. If there was an event that is raised when the login attempt completes, you could subscribe to it and complete a TaskCompletionSource. But that'd only work if there's some sort of notification mechanism built-in.
     
    swifter14 likes this.
  32. swifter14

    swifter14

    Joined:
    Mar 2, 2017
    Posts:
    165
    Now that you say it, there is a call back I can use from FB API,
    I use this in my connect to connect(before the while loop): FB.LogInWithReadPermissions(permissions)
    But their API says:
    public static void LogInWithReadPermissions(
    IEnumerable<string> permissions = null,
    FacebookDelegate<ILoginResult> callback = null
    )
    OK I think that I'm OK with LOGIN because they have a callback,
    They have an example here for LOGIN
    https://developers.facebook.com/docs/unity/examples/

    But the problem remains with FB.LOGOUT() - I need to wait for it to be finished, before continuing and there is no callback
     
    Last edited: Mar 31, 2019
    Suddoha likes this.
  33. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    Typically in APIs like that there is usually a group of global events/delegates you can attach to, one such being something like OnLogout or OnDisconnected.

    Cause you may have multiple decoupled features, features not directly related to the code that sends the log off call, that may want to do something when you are logged off (or are forced off for some other reason, like losing internet connection).
     
    swifter14 likes this.
  34. asadmubashr

    asadmubashr

    Joined:
    Feb 10, 2021
    Posts:
    31
    That's my code not working
    private async void Start()
    {

    Debug.Log("Checking...");
    await CheckUserExist();
    Debug.Log("Checked");

    }


    // Database API Calls
    private async Task CreateNewUser(User NewUser)
    {
    user = NewUser;
    string json = JsonUtility.ToJson(NewUser);
    await reference.Child("users").Child(user_id).SetRawJsonValueAsync(json);

    }

    private async Task GetUserData()
    {
    await FirebaseDatabase.DefaultInstance
    .GetReference("users/" + user_id)
    .GetValueAsync().ContinueWith(task =>
    {
    DataSnapshot dataSnapshot = task.Result;
    user = JsonUtility.FromJson<User>(dataSnapshot.GetRawJsonValue());
    });
    }

    private async Task CheckUserExist()
    {
    await FirebaseDatabase.DefaultInstance
    .GetReference("users/" + user_id)
    .GetValueAsync().ContinueWith(async task =>
    {
    if (task.IsFaulted)
    {
    // Handle the error...
    }
    else if (task.IsCompleted)
    {

    DataSnapshot snapshot = task.Result;
    if (!snapshot.HasChild("coins"))
    {
    User NewUser = new User("Guest", 10000, 0, 0, "1", "Level 1", 0, 0, 0);
    if (PlayerPrefs.GetInt("LoggedInAs") == 1)
    {
    NewUser = new User(PlayerPrefs.GetString("Username"),
    10000, 0, 0, PlayerPrefs.GetString("Image"), "Level 1", 0, 0, 0);

    }
    else if (PlayerPrefs.GetInt("LoggedInAs") == 2)
    {
    NewUser = new User(PlayerPrefs.GetString("apple_username"), 10000, 0, 0, "1", "Level 1", 0, 0, 0);
    }
    else if (PlayerPrefs.GetInt("LoggedInAs") == 3)
    {
    NewUser = new User("Guest", 10000, 0, 0, "1", "Level 1", 0, 0, 0);
    }

    await CreateNewUser(NewUser);
    }
    else
    {
    await GetUserData();
    }
    }

    });
    }