Quantcast
Channel: Code, the Universe and everything... » Async
Viewing all articles
Browse latest Browse all 5

C++ Async Development (not only for) for C# Developers Part V: Cancellation

0
0

In the previous part of the C++ Async Development (not only for) for C# Developers we looked at exception handling in the C++ Rest SDK/Casablanca. Today we will take look at cancellation. However, because in C++ Rest SDK cancellation and exceptions have a lot in common I recommend reading the post about exception handling (if you have not already read it) before looking at cancellation.

C++ Rest SDK allows cancelling running or scheduled tasks. Running tasks cannot, however, be forcefully cancelled from outside. Rather, an external code may request cancellation and the task may (but is not required to) cancel an ongoing operation. (This is actually very similar to how cancellation of tasks works in C# async.)

So, cancellation in its simplest form is about performing a check inside the task to determine if cancellation was requested and – if it was – giving up the current activity and returning from the task. While conceptually simple, implementing a reusable mechanism that allows to “perform a check to determine if cancellation was requested” is not an easy task due to threading and intricacies around capturing variables in lambda expressions. Fortunately, we don’t have to implement such a mechanism ourselves since C++ Rest SDK already offers one. It is based on two classes – pplx::cancellation_token and pplx::cancellation_token_source. The idea is that you create a pplx::cancellation_token_source instance and capture it in the lambda expression representing a task. Then inside the task you use this pplx::cancellation_token_source instance to obtain a pplx::cancellation_token whose is_cancelled method tells you if cancellation was requested. To request a cancellation you just invoke the cancel method on the same pplx::cancellation_token_source instance you passed to your task. Typically you will call the cancel method from a different thread. Let’s take a look at this example:

void simple_cancellation()
{
    pplx::cancellation_token_source cts;

    auto t = pplx::task<void>([cts]()
    {
        while (!cts.get_token().is_canceled())
        {
            std::cout << "task is running" << std::endl;
            pplx::wait(90);
        }

        std::cout << "task cancelled" << std::endl;
    });

    pplx::wait(500);

    std::cout << "requesting task cancellation" << std::endl;
    cts.cancel();
    t.get();
}

Here is the output:

task is running
task is running
task is running
task is running
task is running
task is running
requesting task cancellation
task cancelled
Press any key to continue . . .

One noteworthy thing in the code above is that the pplx::cancellation_token_source variable is captured by value. This is possible because pplx::cancellation_token_source behaves like a smart pointer. It also guarantees thread safety. All of this makes it really easy to pass it around or capture it in lambda functions in this multi-threaded environment.

We now know how to cancel a single task. However, in real world it is very common to have continuations scheduled to run after a task completes. In most scenarios when cancelling a task you also want to cancel its continuations. You could capture the pplx::cancellation_token_source instance and do a check in each of the continuations but fortunately there is a better way. The ppxl::task::then() function has an overload that takes pplx::cancellation_token as a parameter. When using this overload the task will not run if the token passed to this function is cancelled. The following sample shows how this works:

void cancelling_continuations()
{
    pplx::cancellation_token_source cts;

    auto t = pplx::task<void>([cts]()
    {
        std::cout << "cancelling continuations" << std::endl;

        cts.cancel();
    })
    .then([]()
    {
        std::cout << "this will not run" << std::endl;
    }, cts.get_token());

    try
    {
        if (t.wait() == pplx::task_status::canceled)
        {
            std::cout << "task has been cancelled" << std::endl;
        }
        else
        {
            t.get(); // if task was not cancelled - get the result
        }
    }
    catch (const std::exception& e)
    {
        std::cout << "excption :" << e.what() << std::endl;
    }
}

Here is the output:

cancelling continuations
task has been cancelled
Press any key to continue . . .

In the code above the first task cancels the token. This results in canceling its continuation because we pass the cancellation token when scheduling the continuation. This is pretty straightforward. The important thing, however, is to also look at what happens on the main thread. In the previous example we called pplx::task::get(). We did this merely to block the main thread from exiting since it would kill all the outstanding threads including the one our task is running on. Here we call pplx::task::wait(). It blocks the main thread until the task reaches the terminal state and returns the status telling whether the task ran to completion or was cancelled. Note that if the task threw an exception this exception would be re-thrown from pplx::task::wait(). If this happens we need to handle this exception or the application will crash. Finally, for the completeness, we also have code that gets the result of the task (t.get()). It will never be executed in our case since we always cancel the task but in reality more often than not the task won’t be cancelled in which case you would want to get the result of the task.

There is a couple of questions I would like to drill into here:
– Why do we even need to use pplx::task::wait() – couldn’t we just use pplx::task::get()? The difference between pplx::task::wait() and pplx::task::get() is that pplx::task::get() will throw a pplx::task_canceled exception if a task has been cancelled whereas pplx::task::wait() does not throw for cancelled tasks. Therefore by using pplx::task::wait() you can avoid exceptions for cancelled tasks. This also may simplify your code when you handle exceptions in a different place than you would want to handle cancellation
– What happens if we don’t call pplx::task::wait()/pplx::task::get() and the task is cancelled? Sometimes we want to start an activity in the “fire-and-forget” manner and we don’t really care about the result so calling pplx::task::wait()/pplx::task::get() does not seem to be necessary. Unfortunately this won’t fly – if you call neither pplx::task::wait() nor pplx::task::get() on a task that has been cancelled a pplx::task_canceled exception will be eventually thrown which will crash your app – the same way as any other unobserved exception would do.

In the previous example we blocked the main thread to be able to handle the cancellation. This is not good – we use asynchronous programming to avoid blocking so it would be nice if we figured something out to not block the main thread. An alternative to handling a cancellation in the main thread is to handle cancellations in continuations. We can use the same pattern we used before to handle exceptions i.e. we can create a task based continuation in which we will call pplx::task:wait() to get the task status or pplx::task::get() and get a potential pplx::task_canceled exception. The important thing is that the continuation must not be scheduled using the overload of the pplx::then() function that takes the cancellation token. Otherwise the continuation wouldn’t run if the task was cancelled and the cancellation wouldn’t be handled resulting in an unobserved pplx::task_canceled exception that would crash your app. Here is an example of handling cancellation in a task based continuation:

void handling_cancellation_in_continuation()
{
    pplx::cancellation_token_source cts;

    auto t = pplx::task<void>([cts]()
    {
        std::cout << "cancelling continuations" << std::endl;

        cts.cancel();
    })
    .then([]()
    {
        std::cout << "this will not run" << std::endl;
    }, cts.get_token())
    .then([](pplx::task<void>previous_task)
    {
        try
        {
            if (previous_task.wait() == pplx::task_status::canceled)
            {
                std::cout << "task has been cancelled" << std::endl;
            }
            else
            {
                previous_task.get();
            }
        }
        catch (const std::exception& e)
        {
            std::cout << "exception " << e.what() << std::endl;
        }
    });

    t.get();
}

and the output:

cancelling continuations
task has been cancelled
Press any key to continue . . .

This is mostly it. There is one more topic related to cancellation – linked cancellation token sources but it is beyond the scope of this post. I believe that information included in this post should make it easy to understand how linked cancellation token sources work and how to use them if needed.



Viewing all articles
Browse latest Browse all 5

Latest Images

Trending Articles





Latest Images