Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::promise external code, async cancellation

Suppose you have some external synchronous code you cannot modify, and you require it to run async but also require it to be cancellable. If the external code is blocking then I have two options.

A) Fool the user and let my async method return immediately on cancellation, well aware that the code is still running to completion somewhere.

B) Cancel execution

I would like to implement an interface for option B

namespace externallib {
std::uint64_t timeconsuming_operation()
{
    std::uint64_t count = 0;
    for (auto i = 0; i < 1E+10; ++i)
    {
        count++;
    }
    return count;
}
}




template <typename R>
struct async_operation
{

    struct CancelledOperationException
    {
        std::string what() const
        {
            return what_;
        }
    private:
        std::string what_{ "Operation was cancelled." };
    };

    template<typename Callable>
    async_operation(Callable&& c)
    {
        t_ = std::thread([this, c]()
        {
            promise_.set_value(c());  // <-- Does not care about cancel(), mostly because c() hasn't finished..
        });
    }


    std::future<R> get()
    {
        return promise_.get_future();
    }


    void cancel()
    {
        promise_.set_exception(std::make_exception_ptr(CancelledOperationException()));
    }

    ~async_operation()
    {
        if (t_.joinable())
            t_.join();
    }

private:
    std::thread t_;
    std::promise<R> promise_;
};

void foo()
{
    async_operation<std::uint64_t> op([]()
    {
        return externallib::timeconsuming_operation();
    });

    using namespace std::chrono_literals;
    std::this_thread::sleep_for(5s);

    op.cancel(); 
    op.get();
}

In the code above I cannot wrap my head around the limitation of external code being blocking, how, if at all, is it possible to cancel execution early?

like image 514
arynaq Avatar asked Dec 20 '17 14:12

arynaq


3 Answers

Short answer:

Don't cancel/terminate thread execution unless it is mission critical. Use approach "A" instead.

Long answer:

As @Caleth noted, there is no standard nor cross platform way to do this. All you can do is to get a native handle to a thread and use platform specific function. But there are some important pit falls.

win32

You may terminate a thread with TerminateThread function, but:

  • stack variables will not be destructed
  • thread_local variables will not be destructed
  • DLLs will not be notified

MSDN says:

TerminateThread is a dangerous function that should only be used in the most extreme cases.

pthread

Here situation is slightly better. You have a chance to free your resources when pthread_cancel is got called, but:

  • By default, target thread terminates on cancellation points. It means that you cannot cancel a code that doesn't have any cancellation point. Basically, for(;;); won't be canceled at all.
  • Once cancellation point is reached, implementation specific exception is thrown, so resources can be gracefully freed.
  • Keep in mind, that this exception can be caught by try/catch, but it's required to be re-thrown.
  • This behavior can be disabled by pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, nullptr);. But in case cancellation point is not met, resources won't be freed (as for win32)

Example

#include <iostream>
#include <thread>
#include <chrono>

#if defined(_WIN32)
#include <Windows.h>
void kill_thread(HANDLE thread) {
    TerminateThread(thread, 0);
}
#else 
#include <pthread.h>
void kill_thread(pthread_t thread) {
    pthread_cancel(thread);
}
#endif

class my_class {
public:
    my_class()  { std::cout << "my_class::my_class()"  << std::endl; }
    ~my_class() { std::cout << "my_class::~my_class()" << std::endl; }
};

void cpu_intensive_func() {
#if !defined(_WIN32)
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, nullptr);
#endif
    my_class cls;
    for(;;) {}
}

void io_func() {
    my_class cls;
    int a;
    std::cin >> a;
}

void io_func_with_try_catch() {
    my_class cls;
    try {
        int a;
        std::cin >> a;  
    } catch(...) {
        std::cout << "exception caught!" << std::endl;
        throw;
    }
}

void test_cancel(void (*thread_fn) (void)) {
    std::thread t(thread_fn);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    kill_thread(t.native_handle());    
    t.join();
    std::cout << "thread exited" << std::endl;
    std::cout << "--------------------" << std::endl;
}

int main() {
    test_cancel(cpu_intensive_func);
    test_cancel(io_func);
    test_cancel(io_func_with_try_catch);
    return 0;
}

You may see that:

  • The destructor is never called on windows.
  • Removing of pthread_setcanceltype leads to hang.
  • The internal pthread exception could be caught.
like image 79
ivaigult Avatar answered Nov 02 '22 09:11

ivaigult


There is no portable way to end a thread before it wants to.

Depending on your platform, there may be ways of ending a thread, which you will probably need to get std::thread::native_handle to utilise. This is highly likely to lead to undefined behaviour, so I don't recommend it.

like image 32
Caleth Avatar answered Nov 02 '22 10:11

Caleth


You can run that external synchronous code in another process and terminate that entire process. This way the interruption won't affect your process and cause undefined behaviour.

like image 20
Maxim Egorushkin Avatar answered Nov 02 '22 11:11

Maxim Egorushkin