Freigeben über


How to put a PPLTasks continuation chain into a loop

Background

The way to express sequential logic in asynchronous programming with PPLTasks is task continuation chain; however, there is no easy way to construct a loop of continuations with tasks.  In many scenarios, an asynchronous loop can be useful; for example, suppose you want to download a file from a website asynchronously. A typical strategy is to download it chunk by chunk; As shown in Code 1a, to write synchronous code we put the read and write code in a while loop, however, to write asynchronous code we have to dynamically generate the continuation chain, as shown in Code 1b.

while(it's not completed) {

    file.write(networkStream.read());

}

create_task(networkStream.readAsync());

.then(writeFileAsync)

.then([] {

   // if the transfering is not completed,

   // dynamically repeat above tasks.

});

Code 1a

Code 1b

 

Algorithm

The classic algorithm for dynamically generating repeating tasks is “recursive” task creation.  The trick is to put the creation of continuation chain into a function, and “recursively” call itself at the end of the continuation chain, as shown in Code2. Please note that the “recursive” call is not true recursion.It will not accumulate stack since every recursive is made in a new task.

void repeat() {

    create_task(networkStream.readAsync())

    .then(writeFileAsync)

    .then([] {

       if (not_completed())

           repeat();

    });

}

Code 2

The algorithm above has a fatal drawback: It is not composable. We are not able to attach a continuation to the loop generated in Code 2, as we are for normal tasks.

In this particular scenario, you may want to perform some clean up after downloading the file, e.g. close the network connection and file. It is necessary for the loop algorithm to return a task.

If you are familiar with the “task-unwrapping” trick in PPL Tasks, you may improve the algorithm as in Code 3.  This is a really smart trick that recursively unwraps the next iteration of tasks. The Runtime will schedule it in a way that previous iteration’s task will wait for the next iteration’s task to finish before proceeding.  

task<void> repeat(){

    return create_task(networkStream.readAsync())

    .then(writeFileAsync)

    .then([] {

        if (not_completed())

            return repeat();

        else

            return create_task([]{}); // empty task

    });

}

Code 3

 

Figure 1

 

As demonstrated in Figure 1, the continuation attached on first iteration’s task (the task directly returned by first repeat() function call), will be scheduled after all iterations are finished. This method is almost perfect since we are now able to do something like repeat().then(func); , except this algorithm could run cause out of memory if you have a huge number of iterations. It is not hard to tell from Figure 1 that tasks from all iterations will be kept in memory until the loop is finished.

Create_iterative_task

Considering all these potential problems, we provide you with a robust asynchronous loop utility function

task<void> create_iterative_task(std::function<task<bool>()>)

which creates a task that iteratively executes a user defined task over and over again until a terminal condition is satisfied. This function takes a user functor which creates a task of type task<bool> that needs to be executed repeatedly. The Boolean value returned by the user task is the predicate that decides whether the  teration should continue. The task returned by create_iterative_task will not complete until all its iterations have completed.

Like all other tasks, this iterative task also accepts an optional cancellation token to cancel the loop and it will propagate exceptions to its continuation as well. Code 4 has pseudocode that shows you the basic usage of this utility function for asynchronous file downloading. 

cancellation_token ct;

create_iterative_task([] {

    return create_task(networkStream.readAsync())

    .then(writeFileAsync)

    .then([] {

        return is_finished();

    }); // a boolean task

}, ct)

.then(task<void> t) { // continuations gets executed after loop

   try {

       t.get();

   } catch (Exception) {

   }

   // do clean-ups

}

Code 4

 

create_iterative_task gives you following advantages for free, when compared with a simple hand-crafted loop:

  1. It makes your asynchronous logic neat and easy to understand;
  2. You CAN attach continuations to it, and it does not suffer from the out of memory issue;
  3. It can be canceled with a cancellation token;
  4. It will correctly propagate exceptions in the loop to its continuations.

The create_iterative_task utility has already been put into the newVisual Studio Beta PPL Tasks sample pack. The sample pack also includes a real world example ChunkyFileCopySample that demonstrates how to use create_iterative_task to write a program to copy a file chunk by chunk. Please try it now and let us know whether you like it.

Comments

  • Anonymous
    December 18, 2013
    This is awesome. Thank you!

  • Anonymous
    January 21, 2016
    Hi. I'm considering using create_iterative_task, hoping I can make use of it for event loop type code.  What is up with the beta sample pack ("ppltasks_extra.h") these days though? Will it ever make it into PPL proper? It seems a bit risky to create code that relies on it, when I don't know if you support/maintain it or not.

  • Anonymous
    January 22, 2016
    I decided to try use pplwait.h, __await, and __resumable instead (for my message loop). It seems like it is a better replacement for create_iterative_task. It might even become standard C++ soon.