Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inside a loop,does each async call get chained to the returned task using task's continuewith?

The best practice is to collect all the async calls in a collection inside the loop and do Task.WhenAll(). Yet, want to understand what happens when an await is encountered inside the loop, what would the returned Task contain? what about further async calls? Will it create new tasks and add them to the already returned Task sequentially?

As per the code below

private void CallLoopAsync()
{
   var loopReturnedTask = LoopAsync();
}

private async Task LoopAsync()
{
    int count = 0;
    while(count < 5)
    {
       await SomeNetworkCallAsync();
       count++;
    }
}

The steps I assumed are

  1. LoopAsync gets called
  2. count is set to zero, code enters while loop, condition is checked
  3. SomeNetworkCallAsync is called,and the returned task is awaited
  4. New task/awaitable is created
  5. New task is returned to CallLoopAsync()

Now, provided there is enough time for the process to live, How / In what way, will the next code lines like count++ and further SomeNetworkCallAsync be executed?

Update - Based on Jon Hanna and Stephen Cleary:

So there is one Task and the implementation of that Task will involve 5 calls to NetworkCallAsync, but the use of a state-machine means those tasks need not be explicitly chained for this to work. This, for example, allows it to decide whether to break the looping or not based on the result of a task, and so on.

Though they are not chained, each call will wait for the previous call to complete as we have used await (in state m/c, awaiter.GetResult();). It behaves as if five consecutive calls have been made and they are executed one after the another (only after the previous call gets completed). If this is true, we have to be bit more careful in how we are composing the async calls.For ex:

Instead of writing

private async Task SomeWorkAsync()
{
   await SomeIndependentNetworkCall();// 2 sec to complete

   var result1 = await GetDataFromNetworkCallAsync(); // 2 sec to complete
   await PostDataToNetworkAsync(result1); // 2 sec to complete
}

It should be written

private Task[] RefactoredSomeWorkAsync()
{
    var task1 =  SomeIndependentNetworkCall();// 2 sec to complete

    var task2 = GetDataFromNetworkCallAsync()
    .ContinueWith(result1 => PostDataToNetworkAsync(result1)).Unwrap();// 4 sec to complete

    return new[] { task1, task2 };
}

So that we can say RefactoredSomeWorkAsync is faster by 2 seconds, because of the possibility of parallelism

private async Task CallRefactoredSomeWorkAsync()
{
   await Task.WhenAll(RefactoredSomeWorkAsync());//Faster, 4 sec 
   await SomeWorkAsync(); // Slower, 6 sec
}

Is this correct? - Yes. Along with "async all the way", "Accumulate tasks all the way" is good practice. Similar discussion is here

like image 954
Saravanan Avatar asked Nov 26 '15 05:11

Saravanan


2 Answers

When count is zero, new task will be created because of await and be returned

No. It will not. It will simply call the async method consequently, without storing or returning the result. The value in loopReturnedTask will store the Task of LoopAsync, not related to SomeNetworkCallAsync.

await SomeNetworkCallAsync(); // call, wait and forget the result

You may want to read the MSDN article on async\await.

like image 69
Yeldar Kurmangaliyev Avatar answered Oct 07 '22 00:10

Yeldar Kurmangaliyev


To produce code similar to what async and await do, if those keywords didn't exist, would require code a bit like:

private struct LoopAsyncStateMachine : IAsyncStateMachine
{
  public int _state;
  public AsyncTaskMethodBuilder _builder;
  public TestAsync _this;
  public int _count;
  private TaskAwaiter _awaiter;
  void IAsyncStateMachine.MoveNext()
  {
    try
    {
      if (_state != 0)
      {
        _count = 0;
        goto afterSetup;
      }
      TaskAwaiter awaiter = _awaiter;
      _awaiter = default(TaskAwaiter);
      _state = -1;
    loopBack:
      awaiter.GetResult();
      awaiter = default(TaskAwaiter);
      _count++;
    afterSetup:
      if (_count < 5)
      {
        awaiter = _this.SomeNetworkCallAsync().GetAwaiter();
        if (!awaiter.IsCompleted)
        {
          _state = 0;
          _awaiter = awaiter;
          _builder.AwaitUnsafeOnCompleted<TaskAwaiter, TestAsync.LoopAsyncStateMachine>(ref awaiter, ref this);
          return;
        }
        goto loopBack;
      }
      _state = -2;
      _builder.SetResult();
    }
    catch (Exception exception)
    {
      _state = -2;
      _builder.SetException(exception);
      return;
    }
  }
  [DebuggerHidden]
  void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
  {
    _builder.SetStateMachine(param0);
  }
}

public Task LoopAsync()
{
  LoopAsyncStateMachine stateMachine = new LoopAsyncStateMachine();
  stateMachine._this = this;
  AsyncTaskMethodBuilder builder = AsyncTaskMethodBuilder.Create();
  stateMachine._builder = builder;
  stateMachine._state = -1;
  builder.Start(ref stateMachine);
  return builder.Task;
}

(The above is based on what happens when you use async and await except that the result of that uses names that cannot be valid C# class or field names, along with some extra attributes. If its MoveNext() reminds you of an IEnumerator that's not entirely irrelevant, the mechanism by which await and async produce an IAsyncStateMachine to implement a Task is similar in many ways to how yield produces an IEnumerator<T>).

The result is a single Task which comes from AsyncTaskMethodBuilder and makes use of LoopAsyncStateMachine (which is close to the hidden struct that the async produces). Its MoveNext() method is first called upon the task being started. It will then use an awaiter on SomeNetworkCallAsync. If it is already completed it moves on to the next stage (increment count and so on), otherwise it stores the awaiter in a field. On subsequent uses it will be called because the SomeNetworkCallAsync() task has returned, and it will get the result (which is void in this case, but could be a value if values were returned). It then attempts further loops and again returns when it is waiting on a task that is not yet completed.

When it finally reaches a count of 5 it calls SetResult() on the builder, which sets the result of the Task that LoopAsync had returned.

So there is one Task and the implementation of that Task will involve 5 calls to NetworkCallAsync, but the use of a state-machine means those tasks need not be explicitly chained for this to work. This, for example, allows it to decide whether to break the looping or not based on the result of a task, and so on.

like image 28
Jon Hanna Avatar answered Oct 07 '22 02:10

Jon Hanna