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?

Palec
  • 12,743
  • 8
  • 69
  • 138
Germán Diago
  • 7,473
  • 1
  • 36
  • 59
  • 1
    At which point will you access it? Could you add that in the example? – KillianDS Aug 25 '14 at 08:00
  • 1
    why not `std::thread th{&foo}; th.detach();` ? – Piotr Skotnicki Aug 25 '14 at 08:53
  • 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). – stefaanv Aug 25 '14 at 08:55
  • 3
    @stefaanv 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. – KillianDS Aug 25 '14 at 09:15
  • 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 deadlock – Anton Aug 25 '14 at 09:27
  • @KillianDS: I missed the copy to the lambda, so yes, I'm glad I didn't try and answer. – stefaanv Aug 25 '14 at 09:36
  • 1
    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... – Nim Aug 25 '14 at 09:43
  • 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](http://pastebin.com/C7HH4tmL) (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**](http://en.cppreference.com/w/cpp/thread/future/future). – WhozCraig Aug 25 '14 at 09:45
  • 1
    @WhozCraig: `return` statement prefers moving if available. It does move here and does not block inside the function. – Jan Hudec Aug 25 '14 at 09:56
  • @JanHudec that's what I thought. I suspect Nim is right and a move is "not" being done, and therefore blocking on the destructor. – WhozCraig Aug 25 '14 at 10:00
  • @WhozCraig: That's what I first answered and Nim told me that it's wrong. – Jan Hudec Aug 25 '14 at 10:08

2 Answers2

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...

Nim
  • 33,299
  • 2
  • 62
  • 101
  • 1
    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. – Jan Hudec Aug 25 '14 at 09:52
  • @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) – Nim Aug 25 '14 at 09:53
  • @JanHudec The advantage is that with the second version I can "fire and forget" without blocking. That is my point in this case. – Germán Diago Aug 25 '14 at 10:04
  • 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. – Jan Hudec Aug 25 '14 at 10:18
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.

Jan Hudec
  • 73,652
  • 13
  • 125
  • 172
  • 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... :/ ] – Nim Aug 25 '14 at 09:59
  • I think it does not behave the same, due to destructor behaviour in async, combined with `std::future`. – Germán Diago Aug 25 '14 at 10:03
  • @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. – Germán Diago Aug 25 '14 at 10:19
  • @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. – Jan Hudec Aug 25 '14 at 10:20
  • @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. – Germán Diago Aug 25 '14 at 10:31
  • @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. – Jan Hudec Aug 25 '14 at 10:35
  • @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. – Germán Diago Aug 25 '14 at 10:37