• FEATURED

View more

View more

View more

Image of the Day Submit

IOTD | Top Screenshots

The latest, straight to your Inbox.

Subscribe to GameDev.net Direct to receive the latest updates and exclusive content.

Cleanup in Callback-based Asynchronous APIs?

Old topic!

Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

7 replies to this topic

#1SeraphLance  Members

Posted 17 June 2014 - 08:17 PM

I'm not really used to callback APIs (except glut, which I like to pretend never happened) and have traditionally used futures or some kind of thread-pool mechanism for asynchrony in my code.  As such, I don't really understand how this works.

So I've got a "kick-off" endpoint:

void doneHandler(SomeLibraryObjectAsync *caller, other_args)
{
//write to some static memory somewhere.
delete caller;

}

void doStuff()
{
}


This is obviously highly pared-down, but the problem still rears its ugly head -- how can I safely delete the async object without a blocking wait on the main thread?  The delete call in the handler should take care of it, but as far as I can tell there's no way I can know if that delete call is safe, because the object itself is above it in the stack, and without looking at the source code (if it's even available, which in my case it's not) I have no way of knowing whether it's safe to destroy at that moment.

Is it just assumed that all callback-based APIs ensure safe destruction when their last callback runs?  Is there some magical "delete but don't call the destructor until the object I want to destroy gets popped off the callstack-but-not-quite-RAII" thing I've never heard of in C++?

Perhaps I've just drunk the RAII kool-aid so much I can't even reason around objects with non-deterministic lifetimes.

Edited by SeraphLance, 17 June 2014 - 08:18 PM.

#2Samith  Members

Posted 17 June 2014 - 08:30 PM

I'm not quite sure what you're asking. In your example code, "task" is never on the callstack to begin with. A pointer to task is on the callstack, but not the object itself. So, assuming the destructor to SomeLibraryObjectAsync is thread safe and the delete operator is thread safe, it should be fine to delete the object in the callback. The main problem I see is that if the "doStuff" function had more stuff after the task.GoAndCallThisWhenDone() call then task could potentially become a dangling reference.

If you were really concerned about things you could have the doneHandler add the caller pointer to a "dead" list that you run through at some deterministic point on the main thread. That way you can keep all your new/deletes on the same thread.

#3SeraphLance  Members

Posted 17 June 2014 - 08:38 PM

I'm not quite sure what you're asking. In your example code, "task" is never on the callstack to begin with. A pointer to task is on the callstack, but not the object itself. So, assuming the destructor to SomeLibraryObjectAsync is thread safe and the delete operator is thread safe, it should be fine to delete the object in the callback. The main problem I see is that if the "doStuff" function had more stuff after the task.GoAndCallThisWhenDone() call then task could potentially become a dangling reference.

If you were really concerned about things you could have the doneHandler add the caller pointer to a "dead" list that you run through at some deterministic point on the main thread. That way you can keep all your new/deletes on the same thread.

Perhaps I wasn't clear.  The assumption here is that "GoAndCallThisWhenDone" does some asynchronous work.  Maybe It's a thread that does stuff.  Maybe it's an IO interrupt.  Whatever.  The point is that (if it's a thread, but interrupts basically incur the same problem) the call stack will look like this:

- doneHandler()

void SomeLibraryObjectAsync::SomeInternalFunction()
{
//do stuff
doneHandler(this, args);
//do more stuff
}


"do more stuff" could cause the program to crash if the handler deletes the task.

#4Samith  Members

Posted 17 June 2014 - 08:58 PM

The point is that (if it's a thread, but interrupts basically incur the same problem) the call stack will look like this:

- doneHandler()

I see what you're saying now.

It's certainly possible that your stack looks like that on the worker thread, but it's not guaranteed to be like that. The doneHandler could just as easily be called at the end of a function that wraps all the task calls. It's up to the API documentation/samples to indicate what kinds of operations are appropriate from within the callback functions. I don't think there's really a better answer than that. There is no magic delete operator like you suggested in the OP, you've just got to read the documents and look at sample code to determine what the correct usage is.

#5SeanMiddleditch  Members

Posted 17 June 2014 - 09:15 PM

I'm normally really against shared_ptr, but this is a circumstance it handles well: you don't know the exact lifetime of your object, but you need it to be cleaned up when you're done.

With a little more thought you can use the much-better unique_ptr, but making it easy requires generalized lambda capture (a C++14 feature not yet in any shipping version of Visual C++, though it's in the latest GCC and Clang releases).

Game Developer, C++ Geek, Dragon Slayer - http://seanmiddleditch.com

C++ SG14 "Games & Low Latency" - Co-chair - public forums

Wargaming Seattle - Lead Server Engineer - We're hiring!

#6SeraphLance  Members

Posted 17 June 2014 - 09:29 PM

I see what you're saying now.

It's certainly possible that your stack looks like that on the worker thread, but it's not guaranteed to be like that. The doneHandler could just as easily be called at the end of a function that wraps all the task calls. It's up to the API documentation/samples to indicate what kinds of operations are appropriate from within the callback functions. I don't think there's really a better answer than that. There is no magic delete operator like you suggested in the OP, you've just got to read the documents and look at sample code to determine what the correct usage is.

I know it's not guaranteed to be like that.  I was more asking "how do I know it's not like that, or if it is, what can I do about it?"

From the sound of things, the answers are "you don't, unless the docs say so" and "nothing, redesign your architecture".  That's unfortunate, but c'est la vie.  Thanks.

I'm normally really against shared_ptr, but this is a circumstance it handles well: you don't know the exact lifetime of your object, but you need it to be cleaned up when you're done.

With a little more thought you can use the much-better unique_ptr, but making it easy requires generalized lambda capture (a C++14 feature not yet in any shipping version of Visual C++, though it's in the latest GCC and Clang releases).

It's not really an issue of what kind of owning pointer is used.  The problem is that you've got a transfer of control flow where the object you're deleting is technically in use, so the safety of delete/release/whatever is unknown.  In the real world example I ran into, I have an object that owns a bunch of ComPtrs (which are reference counted like shared_ptr) that wrap the whole asynchronous transaction which calls back into it at the end to "delete this".  That's when I realized there was no apparently well-defined way to know if I was actually at the end.  It's unfortunate that the docs are the only outlet in this situation.  I was hoping it was some well-trodden path, but I guess I'll just have to test it a lot and hope for the best.

#7ApochPiQ  Moderators

Posted 17 June 2014 - 10:24 PM

A well-designed API will make it clear when you are allowed to clean up, or possibly just handle cleanup itself internally.

Sadly, most APIs are not well-designed, so you have to fall back to documentation. Oh wait, most people don't document their APIs, either.

This is why callback-based APIs in manual memory management languages are a bad idea.
Wielder of the Sacred Wands

#8SeanMiddleditch  Members

Posted 18 June 2014 - 12:28 AM

It's really not that hard at all. You absolutely don't need garbage collection and you don't need to abandon callbacks or use a different paradigm. Just separate the object that owns state from the operation itself.

shared_ptr<State> pState = make_shared<State>(); // ref = 1

do_async_foo1([pState](){ // ++ ref during lambda construction
intro1(pState);
do_async_foo2([pState](){ // ++ ref during lambda construction
intro2(pState);
do_async_foo3([pState](){ // ++ ref during lambda construction
intro3(pState);
outtro3(pState);
// -- ref at end of scope, probably when the deletion happens if the async operations are all delayed
});
outtro2(pState);
// -- ref at end of scope
});
outtro1(pState);
// -- ref at end of scope
});
// -- ref at end of scope
The problem with using member functions directly is that this doesn't participate in reference counting, so you _must_ bind a shared_ptr to the object and not just this.

Using the above pattern, though, your lambdas could invoke member functions on pState or member functions on objects owned by pState as necessary.

With a little bit of (admittedly non-trivial) work you can also create a helper function similar to std::bind to bind member functions around a shared_ptr allowing you to chain operation on a reference counted object, e.g.

http://goo.gl/BlYOCd

The interesting bit being:

// returns an unnamed functor that can implicitly case to std::function<ReturnType(ParamTypes...)>
template <typename ObjectType, template <typename> class PtrType, typename ReturnType, typename ...ParamTypes>
auto my_bind(PtrType<ObjectType> ptr, ReturnType(ObjectType::*member)(ParamTypes...))
{
// C++14 generalized lambda capture
//return [ptr = std::move(ptr), &member](ParamTypes&&... argv){ return (ptr.get()->*member)(std::forward<ParamTypes>(argv)...); };

// pre-C++14 generalized lambda capture
struct wrapper {
PtrType<ObjectType> ptr;
ReturnType(ObjectType::*member)(ParamTypes...);
wrapper(PtrType<ObjectType> ptr, ReturnType(ObjectType::*member)(ParamTypes...)) : ptr(ptr), member(member) {}
ReturnType operator()(ParamTypes&&... argv) {
return (ptr.get()->*member)(std::forward<ParamTypes>(argv)...);
}
};
return wrapper(std::move(ptr), member);
}
pState->do_async(my_bind(pState, &State::on_done));
You can cobble something together using std::bind and std::mem_fn, too, but you can't make it generic to any number of parameters without some serious contortions.

You can remove the use of an explicit pState object with a bit of appropriate application of lambdas, but I leave that as an exercise for the reader (which should be trivial at this point if you've been following along).

Edited by SeanMiddleditch, 18 June 2014 - 12:38 AM.

Game Developer, C++ Geek, Dragon Slayer - http://seanmiddleditch.com

C++ SG14 "Games & Low Latency" - Co-chair - public forums

Wargaming Seattle - Lead Server Engineer - We're hiring!

Old topic!

Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.