std::thread arguments and copies

Started by
15 comments, last by BitMaster 8 years, 4 months ago

Encountered an interesting scenario, that I'm not sure if there's a guarantee for.


static void DestructionHandler(std::shared_ptr<Object> object) {
 while(!object.unique()) {
  sleep(100 ms);
 }
}

...

void something() {
 std::shared_ptr<Object> object = std::make_shared<Object>();
 std::thread destructionHandler(DestructionHandler, object);
 destructionHandler.detach();
}

Is the loop guaranteed to exit, or could the underlying OS code that calls DestructionHandler have an extra reference to the object?

Is the loop guaranteed to exit, or could the underlying library code that calls DestructionHandler have an extra reference to the object (extra copy of the shared_ptr)?

Advertisement
Ask yourself this: what if the underlying OS is not written in C++?

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

I'm more concerned with: could it possibly be? :)

Though my question was not properly worded using "OS" when the directly underlying caller is probably not the OS but the library implementation (I guess..).

Reading through the standard it seems to be guaranteed, it says that the types of the arguments have to be move-constructible and use what they call "decay copy", so I assume it will have to work at least logically something like copy-constructing to some allocated memory that is passed to the new thread, and then when the library implementation takes over on that thread it will move-construct those objects onto the topmost stack frame in the arguments to the thread handler function.. so I guess it is guaranteed to not hold extra copies.

Though I'm not well versed enough in these rules that I completely understand if there is actually a guarantee that this is how it _will_ work and not just how it _must be possible for it to work_...

Maybe I should have asked the inverse question :-)

Even if the OS implementation of thread parameters is C++, how could it possibly know what to do with arbitrary things passed around via thread parameters?

There are in fact two important things here, which you've hinted at, but are worth pointing out for clarity: the OS does nothing with the memory you pass it (it can't!) and the library is free to do whatever it likes when handing off from an OS thread entry procedure to the thread proc you gave it.

I don't intuitively feel like the standard needs to guarantee anything with regards to copies passed between threads, aside from what it always guarantees about copies. But the standards committee does not, for better or worse, have any human-like notion of making intuitive sense, so YMMV.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]


and the library is free to do whatever it likes when handing off from an OS thread entry procedure to the thread proc you gave it

That's the main question, if "whatever it likes" includes keeping a copy of the object around for whatever reason, or without reason.

I'm not really concerned with temporary copies as such, just non-destructed copies. I may have been unclear about this.

Another way to write the question:


void ThreadProc(std::shared_ptr<Object> object) {
 object.reset();
 while(true) {
 }
}

void test() {
 std::thread t(ThreadProc, std::make_shared<Object>());
 t.detach();
}

Will the Object whose constructor is called when make_shared creates the object ever have its destructor called?

Again, not sure if it makes sense to even care.. just simpler if it was guaranteed to be destructed.

If not, objects that I really want to always be destructed even when the thread continues to run have to be passed to the thread in another way than the thread arguments.

Seems somewhat senseless for a copy to be kept around somewhere, but that hasn't really stopped anything before :)

Ask yourself this: what if the underlying OS is not written in C++?
Even if the OS implementation of thread parameters is C++, how could it possibly know what to do with arbitrary things passed around via thread parameters?

Why is the OS relevant? We're in C++ library land, not OS land.
I can write a C++ wrapper around the Windows/Linux OS threading APIs that respects the C++ object model... I would assume the C++ standard library also does so.

std::thread copies/moves all the constructor arguments to temporary storage, then launches the underlying OS thread, then invokes your callback in that thread (passing the args from the temporary storage).
So I'm not sure, but this may be an infinite loop:
1) something has a shared-ptr
2) std::thread's constructor copies it (now two ref's)
3a) DestructionHandler copies it (now three ref's)
4a) something exits and destructs it's shared-tr (now two ref's)
OR
3b) something exits and destructs it's shared-tr (now one ref)
4b) DestructionHandler copies it (now two ref's)

You can pass the arguments by reference instead, and have the main thread block until DestructionHandler runs far enough to make a value-copy from the reference-argument -- that would eliminate any hidden temporaries. That's actually what I do in my own OS-thread wrapper, so that I can pass objects by value without having to keep extra copies around for longer than necessary.
My reasoning was on similar lines of Hodgman, however since a sensible implementation should prefer moving over copying you should never have more than one reference counted anywhere and it should work. If it did not work like that, transferring a std::unique_ptr<> instead of std::shared_ptr<> would be impossible and although I cannot remember ever having done that I'm pretty sure it should be possible. That said, at least the documentation over on cppreference.com is not as clear as it could be and I'm unsure if it is guaranteed behavior. Maybe going to the actual standard would help but I'm currently in no mood to do that digging.

Thanks for your replies!
The unique_ptr example is a good one.. that wouldn't work if anything but a move constructor was used obviously.. so depending on template argument handling move-construction for everything on the new thread is probably the only thing that makes sense...

1: Either move or copy all args into the allocated temp storage depending on their template types (still on main thread)

1b: Including the ThreadProc (it's treated specifically by the standard, could be a lambda, std::function etc)

2: Start thread

3: (on thread 2) std::move all args from temp storage in a call to the user-defined ThreadProc

So if that's how it always works for all args then exactly one copy will exist on thread 2 and it will be the arguments to ThreadProc on the stack.

If however the lib implementation was done so that (3) did not properly use move-construction for an argument then there could be a problem.

Thought of another case that must be kept in mind for this, if using a lambda or function wrapper object, namely how operator () is implemented in respect to its arguments (if the thread proc is not directly operator() )

Probably best either way to do like Hodgman says with refs for clearity, or the easiest might be to pass in a unique_ptr<shared_ptr>, to wrap it into something that couldn't possibly be copied anywhere on the way.

The arguments passed to the thread constructor need to be copyable (at least in C++11). So unique_ptr won't work.

And, at least in the vs 2013 library implementation, the ref count of the object is 2 when the main thread has released its reference - so unique() will never be true.

This topic is closed to new replies.

Advertisement