1

I am executing n task using std::for_each and these tasks can be canceled. So for doing that I have a flag that is set to true if tasks to be canceled which in turn throws some exception in the task's code. And it works fine if I use normal std::for_each, but it aborts if I use any of std execution_policy. Is there a way to stop my code from aborting?

#include <execution>
#include <array>
#include <chrono>
#include <exception>
using namespace std::chrono_literals;

auto main(int argc, char* argv[]) -> int {
    std::array<int, 5> x {1, 2, 3, 4, 5};
    std::atomic<bool> toBeCancelled = true;
    std::for_each(std::execution::par, std::begin(x), std::end(x), [&](const int& x) {
        std::this_thread::sleep_for(2s);
        if (toBeCancelled)
            throw std::runtime_error("taskCancelled");
    });
    return 0;
}

The number of tasks can be 1000-10000. I usually don't use exceptions, instead I cover major code in "if condition". but here task count is so large, I thought running code outside of "if condition" is not worth it.

  • If you ever modified `toBeCancelled`, it would be a data race with all of the code attempting to read it. – Nicol Bolas Jun 21 '21 at 05:33
  • To follow on to the good Sir Bolas' point, using exceptions for control flow is [not good practice](https://stackoverflow.com/questions/729379/why-not-use-exceptions-as-regular-flow-of-control). You may be interested in [this thread](https://stackoverflow.com/questions/9711414/what-is-the-proper-way-of-doing-event-handling-in-c). In particular, the section on [condition variables](https://en.cppreference.com/w/cpp/thread/condition_variable). – Dr. Watson Jun 21 '21 at 05:35
  • Catch the exception before returning from the thread function. – Richard Critten Jun 21 '21 at 05:39
  • @NicolBolas toBeCancelled can be changed once in the original code. So no data race issue – Prateek Chokse Jun 21 '21 at 05:44
  • @Dr.Watson I usually don't use exception but here the number of tasks can in the range of 1000-10000, if I don't use exception for loop will run for all task even if it is not required. Even if it major portion is inside if condition but it will still run. – Prateek Chokse Jun 21 '21 at 05:53
  • 1
    If a thread function terminates by throwing an exception, the program aborts. There is no way around it. – n. m. could be an AI Jun 21 '21 at 05:54
  • 1
    "toBeCancelled can be changed once in the original code" Once is quite enough. If it is changed before the threads are started, it is useless. If it is changed after the threads are started, it is UB. You need to make it at least atomic. – n. m. could be an AI Jun 21 '21 at 05:57
  • @n.1.8e9-where's-my-sharem. in original code it is. – Prateek Chokse Jun 21 '21 at 06:01
  • @PrateekChokse: "*in original code it is.*" It is what? Changed during the operation? I don't see any atomics or mutexes or anything to prevent data races. If one thread changes a variable, and another thread accesses it, that's a data race unless something prevents it from being a data race. – Nicol Bolas Jun 21 '21 at 06:10
  • this is code is just to show my current logic, in my main code which is not this, there I am using atomic_bool – Prateek Chokse Jun 21 '21 at 07:15
  • 1
    I think you would need to handle exception inside the lambda. – warchantua Jun 21 '21 at 07:18

2 Answers2

2

You can't let an exception out of the callable you pass to for_each, under pain of std::terminate, as you have seen. But you don't need an exception, you know that you are cancelling tasks.

[&](const int& x) {
    std::this_thread::sleep_for(2s);
    if (toBeCancelled)
        return;
}

Aside: in C++20 we get std::stop_token which is intended for this sort of signalling.

Caleth
  • 52,200
  • 2
  • 44
  • 75
  • Concise and incisive. [std::stop_token](https://en.cppreference.com/w/cpp/thread/stop_token) is new to me, thanks for pointing it out! – Dr. Watson Jun 21 '21 at 08:05
0

From cppreference execution policies:

During the execution of a parallel algorithm with any of these execution policies, if the invocation of an element access function exits via an uncaught exception, std::terminate is called, but the implementations may define additional execution policies that handle exceptions differently.

So if it throws, your program will terminate.

How to handle exception for parallel std algorithms

Then cppreference std::thread has the answer to your question:

[...] function may communicate its return value or an exception to the caller via std::promise or by modifying shared variables (which may require synchronization, see std::mutex and std::atomic).

Use a shared variable and remember about locking. There's even an example in cppreference execution policies that increments an integer, with a bit of tweaking your code may look like:

std::mutex m;
std::vector<std::exception> throwed;
std::for_each(std::execution::par_unseq, std::begin(a), std::end(a), [&](int) {
     std::this_thread::sleep_for(2s);
     {
        std::lock_guard<std::mutex> guard(m);
        if (toBeCancelled) {
            throwed.push_back(std::runtime_error("taskCancelled"));
        }
    }
});

or you can catch the exception and push_back it then.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111