2

I have a class which calls an asynchronous task using std::async in his constructor for loading its content. ( I want the loading of the object done asynchronously )

The code looks like this:

void loadObject(Object* object)
{
 // ... load object
}

Object::Object(): 
{
    auto future = std::async(std::launch::async, loadObject, this);
}

I have several instances of these objects getting created and deleted on my main thread, they can get deleted any time, even before their loading has finished.

I'd like to know if it is dangerous to having object getting destroyed when it is still getting handled on another thread. And how can I stop the thread if the object gets destroyed ?

EDIT: The std::future destructor does not block my code with the VS2013's compiler that I am using due to a bug.

thp9
  • 376
  • 2
  • 15

3 Answers3

4

As MikeMB already mentioned, your constructor doesn't finish until the load has been completed. Check this question for how to overcome that: Can I use std::async without waiting for the future limitation?

I'd like to know if it is dangerous to having object getting destroyed when it is still getting handled on another thread.

Accessing object's memory after deletion is certainly dangerous, yes. The behaviour will be undefined.

how can I stop the thread if the object gets destroyed ?

What I recommend you to take care of first, is to make sure that the object doesn't get destroyed while it's still being pointed at by something that is going to use it.

One approach is to use a member flag signifying completed load that is updated in the async task and checked in the destructor and synchronize the access with a condition variable. That will allow the destructor to block until the async task is complete.

Once you've managed to prevent the object from being destroyed, you can use another synchronized member flag to signify that the object is being destroyed and skip the loading if it's set. That'll add synchronization overhead but may be worth it if loading is expensive.

Another approach which avoids blocking destructor is to pass a std::shared_ptr to the async task and require all Object instances to be owned by a shared pointer. That limitation may not be very desireably and you'll need to inherit std::enable_shared_from_this to get the shared pointer in the constructor.

Community
  • 1
  • 1
eerorika
  • 232,697
  • 12
  • 197
  • 326
3

There is nothing asynchronous happening in your code, because the constructor blocks until loadObject() returns (The destructor of a future returned by std::async implicitly joins).

If it would not, it would depend on how you have written your code (and especially your destructor), but most probably, your code would incur undefined behavior.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
MikeMB
  • 20,029
  • 9
  • 57
  • 102
  • I have already heard about this destructor joining thing, I had doubts, but I have tested my program and It seems to be really asynchronous. I have an 'update() ' member on my object which gets called every frame and tells me if the objects is loaded ... and I can see that it prints that it isn't during several frames after construction. – thp9 Apr 11 '15 at 22:14
  • _"but I have tested my program and It seems to be really asynchronous"_ Then you tested it wrong. – Lightness Races in Orbit Apr 11 '15 at 22:15
  • Then I guess I still have no idea what asynchronous means. How can you explain that post-constructor code is executed while the object is not loaded yet if constructor is blocking ? – thp9 Apr 11 '15 at 22:21
  • 1
    Plus by setting std::future as my class my member (so it is no more getting destroyed ) I have the exact same result. I'll try to make an example. – thp9 Apr 11 '15 at 22:23
  • @user3134405: _"How can you explain that post-constructor code is executed while the object is not loaded yet if constructor is blocking ?"_ I can't. I propose that your observations are in error! – Lightness Races in Orbit Apr 11 '15 at 22:25
  • 1
    @user3134405: What compiler and what flags are you using? I believe. there was a proposal by Herb Sutter to change that behavior of `std::async` for c++14. As far as I know, it got dropped, but maybe there are some compilers that implement `std::async` in a non conforming manner. – MikeMB Apr 11 '15 at 22:27
  • I am using Visual Studio 2013's compiler. – thp9 Apr 11 '15 at 22:37
  • @user3134405: And the MCVE? – MikeMB Apr 11 '15 at 22:38
  • Sorry what is MCVE ? Here is the example I've made: http://pastebin.com/iVGrHGVc , output is "TEST" then "DONE". My command line options : http://pastebin.com/sWvCXQM8 – thp9 Apr 11 '15 at 22:48
  • @MikeMB, yes, MS wanted to change the behaviour, and they implemented their preferred behaviour assuming their proposal would be accepted. It wasn't. So with at least some versions of Visual Studio `std::future` does not block in its destructor when it refers to an asynchronous task. That's non-conforming. – Jonathan Wakely Apr 11 '15 at 22:59
  • @JonathanWakely: Thats what I had in mind too, but interestingly, the documentation for [VS2013](https://msdn.microsoft.com/en-us/library/hh920568.aspx) claims that the future's destructor would block. So I'm wondering, whether the documentation is wrong (my assumption) or if it is a compiler bug, or if there is something else in the OPs example that I'm overlooking. – MikeMB Apr 11 '15 at 23:34
  • 1
    Apparently it will be fixed on the next major version : https://connect.microsoft.com/VisualStudio/feedback/details/810623 – thp9 Apr 12 '15 at 09:24
-1

Yes it is dangerous to having object getting destroyed when it is still getting handled on another thread

You can implement a lot of strategies actually depending on requirements and desired behaviour.

I would implement sort of pimpl strategy here, that means that all actual data will be stored in the pointer that your object holds. You will load all the data to the data-pointer-object and store it in the public-object atomically.

Techincally speaking object should be fully constrcuted and ready to use by the time the constrcutor is finished. In your case data-pointer-object will still probably be not ready to use. And you should make your class to handle correctly that state.

So here we go:

class Object
{
   std::shared_ptr<Object_data> d;
   Object::Object(): 
      d(std::make_shared<Object_data>())
   {
        some_futures_matser.add_future(std::async(std::launch::async, loadObject, d));

   }
}

Then you make atomic flag in your data-object that will signal that loading is complete and object is ready to use.

class Object_data
{
    // ...
    std::atomic<bool> loaded {false};
};

loadObject(std::shared_ptr<Object_data> d)
{
    /// some load code here
    d->loaded = true;
}

You have to check if your object is constrcuted every time when you acces it (with thread safe way) through loaded flag

Lol4t0
  • 12,444
  • 4
  • 29
  • 65
  • 2
    You did not do anything to make safe destroying an object while it's being used elsewhere (which is impossible, as I've already stated). All you did is add a _block_ to _defer_ the object's destruction until it's _not_ being used elsewhere. – Lightness Races in Orbit Apr 11 '15 at 22:47
  • "some_futures_matser" - what is this? – sehe Apr 11 '15 at 22:47
  • 1
    "some_futures_matser" Just to not auto joing – Lol4t0 Apr 11 '15 at 22:48
  • @LightningRacisinObrit , the questiong only states that object will be used or deleted somewhere and in process of data loading. So it can be deleted before data loading is complete. How is it unsafe here? – Lol4t0 Apr 11 '15 at 22:50
  • @Lol4t0: In the comments under the question, you claimed to be posting this answer to defend the statement that _"it is nothing wrong with deleting smth that not loaded yet"_. That statement is factually inaccurate, and this answer does not prove otherwise. – Lightness Races in Orbit Apr 11 '15 at 22:52
  • @LightningRacisinObrit, It _does_ prove. You can delete `object` before `loadData` is complete. (1) It will not block. (2) It looks safe. What's wrong? If it is not safe, please point some opportunity for this to break. – Lol4t0 Apr 11 '15 at 22:54
  • @Lol4t0: No, it does not prove anything. You've craftily made it so that `loadData` does not access the object after the object has been destroyed. So how does that prove anything about accessing the object after the object has been destroyed? It doesn't. Just because a function is called "loadData" doesn't mean you're demonstrating anything whatsoever about actually loading data. – Lightness Races in Orbit Apr 11 '15 at 22:56
  • @LightningRacisinObrit we are talking about loading object through `loadData` function. That is the point of question. The problem in the question is indeed in accessing object with `loadData` after is has been destroyed. Not arbitrary other code. – Lol4t0 Apr 11 '15 at 23:04
  • @Lol4t0: You're moving the goalposts. You made a statement, and claimed to be backing up that statement, but failed to do so. Now you're claiming to be talking about something else. Are you Vlad? – Lightness Races in Orbit Apr 11 '15 at 23:04
  • My statement was, again, in " it is nothing wrong with deleting smth that not loaded yet. You just have to load it atomically". So there is the solution. – Lol4t0 Apr 11 '15 at 23:08
  • I assume `loadObject` is meant to refer to `d->mutex` and `d->dropped`? And the destructor is meant to refer to `constructing->dropped`? – Jonathan Wakely Apr 11 '15 at 23:08
  • Where you say _"And you should make your class to handle correctly that state"_, I suggest explaining how. When trying to access the `Object_data` the first thread must check whether `d` is set, and that can only be done with the mutex locked, e.g. `lock l(constructing->mutex); if (d) { /* the data is loaded */ } else { /* not loaded yet */ }` – Jonathan Wakely Apr 11 '15 at 23:18
  • @JonathanWakely, ugh. I'd better use some atomic flag for this actually. Because doing it with mutex _every time_ can be a bit expensive. – Lol4t0 Apr 11 '15 at 23:22
  • But then you need to check the flag and update the `unique_ptr` atomically. It would be much simpler to get rid of the `constructing` raw pointer, change the `unique_ptr` to `shared_ptr`, and replace `mutex` and `dropped` with `atomic loaded`, so the `shared_ptr` is always valid, but isn't ready to use until `d->loaded` is true. – Jonathan Wakely Apr 12 '15 at 11:14
  • @JonathanWakely I believe you are right here. I do not like shread_ptr's generally because they are overused I think, but not in this case. – Lol4t0 Apr 12 '15 at 17:39