5

I'm currently working on class with some async tasks under the hood. In fact I need to delegate few tasks to asynchronous execution and be sure that all of them are finished before class was destroyed, I was thinking about await all the tasks until finish inside class destructor. While research existing approaches used inside std library of C++ language. I was curios about different ways of asynchronous implementation.

For example std::thread doesn't wait anything inside destructor. Following code will fail with executable termination:

void sleep() {
    std::this_thread::sleep_for(std::chrono::seconds(5));
}

void execute_thead() {
    auto thread = std::thread(&sleep);
}

So you forced to call join() for the thread you've created. Overall it's look more obvious to me, like: "Hey! You've created async task then be so kind and do manual await of it". But anyway throwing exception in case you forgot to call join() or detach() may be unexpected;

On other hand, let's look on std::async. Following code will await result inside future destructor:

void execute_async() {
    auto result = std::async(std::launch::async, &sleep);
}

It may be not obvious that code code will hang when going out of scope, while waiting till all tasks is complete. It may be hard to find actual bottleneck while refactor or optimizing the code. But advantage is that you don't need to care about unfinished tasks.

Be aware, here I'm not discussing about thread-based vs task-based strategy. Formally I'm asking if implicit await is better than explicit one and is it overall a good way to write asynchronous code.

Liastre
  • 171
  • 1
  • 6
  • Does the async task actually have to complete before the object destruction itself completes? Or could you just trigger the async task and move on? In other words, are you only trying to make sure that the async task completes when the object is destroyed? – Mike Partridge Dec 11 '19 at 15:04
  • Hey @MikePartridge, the answer within the question :) _> In fact I need to delegate few tasks to asynchronous execution and be sure that all of them are finished before class was destroyed_ All tasks must be completed before actual class will be destroyed to move next. – Liastre Dec 11 '19 at 15:37
  • That sounds like a synchronous task, not an asynchronous one. – Robert Harvey Dec 11 '19 at 16:18
  • 1
    Nitpick: `std::thread::~thread` calls `std::terminate`, it doesn't `throw` – Caleth Dec 11 '19 at 16:55
  • @RobertHarvey that class used with synchronous code but sends _few_ asynchronous heavy tasks to execute inside and I expecting all of them done before exit from method where class was created, because following logic may depend on it. – Liastre Dec 11 '19 at 17:25
  • How would you make that implicit await explicit? Are there any benefits to the implicit await that exceed the benefits of code clarity? Maybe you could just put a comment in there to let Future Programmer know what is happening? – Robert Harvey Dec 11 '19 at 17:29
  • @RobertHarvey that's why I'm here actually :) Asking for an advice and maybe a better approach. I thought to minimize mistakes if class was not awaited, guarantee that class do not left any pending work like ```std::future``` do. – Liastre Dec 11 '19 at 17:40
  • 3
    Well, Future Programmer is going to find `auto result = std::async(std::launch::async, &sleep);` in a destructor very counterintuitive, especially if you do nothing with `result`. – Robert Harvey Dec 11 '19 at 17:43
  • Did you consider exception safety? Waiting in the destructor will also happen in case of an exception, which may or may not be desired. – pschill Dec 11 '19 at 22:34
  • @RobertHarvey I agreed with you, it may be pretty confusing, but would be better to throw exception or do terminate in the destructor in case we destroyed object with pending tasks? P.S.: promises result is actually used after retrieving. – Liastre Dec 12 '19 at 07:39
  • I think that, if you're going to take this approach, you need to leave an explanatory comment on the code so that Future Programmer can work out what is going on. – Robert Harvey Dec 12 '19 at 15:43
  • Hey @pschill ! I'm not quite understand what you mean. ```std:::future``` result will be returned immediately from ```std::async``` in case of exception, and exception will be re-thrown only in case you call ```get()``` on it. ```std::thread``` will cause terminate. There no any as you said *waiting in the destructor in case of exception*. Can you clarify? Anyway it's a good point, since I absolutely sure throwing/handling an exception inside destructor is a bad practice – Liastre Dec 16 '19 at 10:57
  • 1
    @Liastre Take the following example: `std::thread t(foo); bar(); t.join();`. This code is broken if `bar()` throws an exception, because then, `t.join()` is not called. However, if `std::thread` automatically joined in the destructor (RAII), you would not have that problem. So an explicit `join()` function makes it harder to ensure exception safety if you compare it to an implicit join in the destructor. – pschill Dec 16 '19 at 14:36

2 Answers2

1

Is it good approach to await async tasks in object destructor?

Personally I would answer that question with "No".
The reason for that answer is simple: This question shouldn't ever arise.

If you run into a situation where that question arise, then there is something wrong with your code design IMHO. Either the async task should not require the object to stay alive until it has finished, in that case there is no need to wait for the task. Or it should never be possible for the object to die before the task has finished, in which case the question doesn't arise either.

Mecki
  • 1,818
  • 12
  • 17
  • I think this answer could be phrased more neutrally; as is, I think it is maybe a little cruel. For the content, could we add more detailed examples or explanations for the two cases you list? Since we’re dealing with concurrency and C++ lifetimes, there’s a lot of ambiguity to read through ;) – gntskn Mar 24 '20 at 11:34
  • @gntsk Samples would be way too big as you cannot do that with 3 lines of code. If you don't know how to implement it as you don't understand what I wrote or don't know how something can be done in C++, ask a concrete new question about it, either here or on StackOverflow, as that's what theses places are all about. Yet they are no places where other people will solve your programming tasks for free. And no, that is not cruel, that's actually from their code of conduct. – Mecki Mar 24 '20 at 12:45
1

The primary reason for asynchronous task-like objects to have their destructors wait on the completion of the task is not so that someone can easily write an "asynchronous" task that immediately terminates. It's there for safety reasons; indeed, it's for the exact same safety reasons that cause RAII to be used in any other cases.

These two cases are equally bad:

int *ptr = new int;
do_something();
delete ptr;
---
thread th(...);
do_something();
th.join();

What happens if do_something emits an exception? The thread th will be lost, and any connection to it dropped on the floor and forgotten. Just like the memory pointed to by ptr.

It's reasonable to have a RAII object release its resource, making it so that it is no longer managed and must be manually cleaned up. It is far less defensible to make this the default behavior.

So the question ultimately boils down to this: how safe do you want your code to be?

Nicol Bolas
  • 11,813
  • 4
  • 37
  • 46