Will this async trick work or the state will be dangling when I access it?
Asked Answered
E

2

13

I am facing a situation where it would be nice to launch an std::async operation totally asynchronously.

future<void> MyClass::MyAsyncFunc() {
    std::future<void> f = std::async(...);
    return f;
}  // The future goes out of scope, will block.

The problem is that the function will block at the end if I don't save the future. I would like this not to happen.

This would prevent the std::future to call its destructor at the end of the function's scope:

shared_ptr<future<void>> MyClass::MyAsyncFunc() {
    auto shared_ftr = std::make_shared<std::future<void>>();
    *shared_ftr = std::async([shared_ftr]() {...});
    return shared_ftr;
}

Could this possibly work? What happens when I don't save the result in a variable?

Epoxy answered 25/8, 2014 at 7:54 Comment(11)
At which point will you access it? Could you add that in the example?Lionel
why not std::thread th{&foo}; th.detach(); ?Holzer
Except for the memory usage (and extra reference count, assuming ftr should be shared_ftr), I don't see any functional difference in the 2 code snippets, the destructor will be called, so I would expect the same behavior. (no answer, because I'm not sure and I haven't checked it).Primateship
@Primateship the idea is that the destructor in MyAsyncFunc will not block because it will only decrease the refcount, which should only go to 0 if the async execution is already stopped.Lionel
The question in other words: "is it guaranteed that lambda instance will not be destroyed before assignment of the result in std::async", otherwise, it might lead to a deadlockInconsequent
@KillianDS: I missed the copy to the lambda, so yes, I'm glad I didn't try and answer.Primateship
My question is - without holding on to the future, how could you possibly know it has completed at all? For example the above approach will work (i.e. return will not block...) but at the call site, as execution will continue and if you don't wait, you won't know it has completed. Somehow you have to guarantee that the application will continue to execute to allow the async task to complete...Dickman
You may want to add your platform and toolchain to your question, as well as a caller-side view of this. And I'm genuinely curious what happens when you run this (though you may have to do some sleep surgery if you're on a Windows platform). I didn't think std::future even supported copy-construction. Last I checked it doesn't.Depside
@WhozCraig: return statement prefers moving if available. It does move here and does not block inside the function.Laurelaureano
@JanHudec that's what I thought. I suspect Nim is right and a move is "not" being done, and therefore blocking on the destructor.Depside
@WhozCraig: That's what I first answered and Nim told me that it's wrong.Laurelaureano
D
6

Here is a fully fledged example. This pattern does work, I use it extensively with boost asio and asynchronous operations.

#include <chrono>
#include <iostream>
#include <future>
#include <memory>
#include <thread>

std::shared_ptr<std::future<int>> get_task()
// std::future<int> get_task() // rely on move, future supports move
{
  auto f = std::make_shared<std::future<int>>();
  //std::future<int> f = std::async(std::launch::async, [] {
  *f = std::async(std::launch::async, [f] {
    (void) f;
    std::cout << "calculating" << std::endl;
    for (int x = 0; x < 10; ++x)
      std::this_thread::sleep_for( std::chrono::milliseconds( 200 ) );
    std::cout << "done." << std::endl;
    return 100;
  });

  return f;
}


int main(void)
{
  std::cout << "getting task" << std::endl;
  //auto f = get_task(); <-- the future is moved not copied, so there is no block here
  get_task();
  std::cout << "waiting" << std::endl;
//  f.wait(); <-- now wait for it to complete...
//  std::cout << " got: " << f.get() << std::endl;
  // Wait for the truly async task to complete...
  std::this_thread::sleep_for(std::chrono::milliseconds(3000));
}

The only concern I'd express is that wait at the end, without capturing the future (whether it's moved or via shared_ptr), you have no way to stop the app from terminating before the task completes...

If you have some other way of ensuring continuation, then the shared_ptr approach will work fine. Else, go with the moved future, it's cleaner...

Dickman answered 25/8, 2014 at 9:49 Comment(4)
I don't think the shared_ptr is useful there. The future can be moved around freely and do the right thing and you don't seem to take any advantage of the fact that the pointer is shared.Laurelaureano
@JanHudec, you do take advantage of it, the lambda capture will ensure that the shared_ptr is not destroyed on return from get_task(), and will only be destroyed at the end of the lambda itself... A reference to the future is held with the lambda - this hack is useful till we get lambdas supporting move semantics (afaik)Dickman
@JanHudec The advantage is that with the second version I can "fire and forget" without blocking. That is my point in this case.Theretofore
Hm, I understand now. Now while that appears to actually be answer to the question, it does not seem to suggest anywhere that "ignoring" the future is the intent.Laurelaureano
L
2
future<void> MyClass::MyAsyncFunc() {
  std::future<void> f = std::async(...
  return f;
} //future out of scope, will block

and

shared_ptr<future<void>> MyClass::MyAsyncFunc() {
    auto shared_ftr = std::make_shared<std::future<void>>();
    *shared_ftr = std::async([]() {...});

    return shared_ftr;
}

are equivalent. The later will work exactly when the former will.

The future in the function that goes out of scope is moved from and therefore can't block. The blocking most likely happens in the calling function, which you have not shown.

Laurelaureano answered 25/8, 2014 at 9:55 Comment(7)
The latter is not the same, if at the call site, the returned value is not saved, they behave differently due to the lambda capture of the shared_ptr (the future is not destroyed till the lambda finishes..) [This is my current understanding and of course I could be completely wrong, if I am, I will have to refactor all my asio code which works this way... :/ ]Dickman
I think it does not behave the same, due to destructor behaviour in async, combined with std::future.Theretofore
@Jan Hudec, the difference is subtle: if I want to fire and forget without getting my future in a variable, the first version will block.Theretofore
@GermánDiago: That you should have asked that. The comment "future out of scope, will block" is incorrect. It's not out of scope there. It is out of scope in the caller that wants to ignore it. If you made the function void, it would be clear. But you are returning it. For purpose of returning it, they are the same.Laurelaureano
@JanHudec I didn't show the calling code, it is true. What I wanted is think of what is happening. The two possibilities are likely, so I think it is a possibility to think of that without explicitely mentioning about it. Anyway, thanks for your feedback, maybe seen from my head I see it another way since I know my own problem better than the others.Theretofore
@GermánDiago: The problem is nothing is happening inside the function. It will move the future to the return temporary. The blocking only occurs at the moment you decide to ignore the future in the caller. And you didn't mention you are ignoring it.Laurelaureano
@JanHudec As I told you, I have a better knowledge of my own problem, so maybe I should have been more explicit. But if you take a closer look, there is no point in even having a second version, the one with a shared_ptr, if it is not because sometimes I want to fire and forget: in this case the 1st version would have been enough.Theretofore

© 2022 - 2024 — McMap. All rights reserved.