Hello everyone, I am new to this list and a little new to Boost. I have a question on program design when using the excellent boost shared pointer's library. I am converting my old network library that uses explicit memory management to use shared_ptr where appropriate. So far this has gone well but I am having problem with cycles. This post is more of request for comments on my thought process, and if there is a "traditional" or better way of handling these issues. The question in general is "How to deal with the cycles that come about from event generator/listener designs?" The problem in more specific terms with my thinking follows. I'm having a problem with trying to mitigate reference cycles in a event generator/event listener paradigm. One creates a listener object, and registers itself as a listener of that object. As a result of the event, it may want to invoke methods of that object, so it contains a reference to that object. So Generator contains a reference to Listener, and Listener contains a reference to Generator. This is a problem with reference counted pointers. People tell me that cycles in general signify a flaw, so perhaps my fundamental design is indeed flawed. A real example from my library is a Connection object representing a network connection. Library user derives a new class implementing the ConnectionListener interface and registers it with Connection. When a packet arrives an event in the listener is invoked, and let's say the listener wants to process the packet and send a packet in response. It does this by passing a packet to the Connection. Thus the Connection refers to the listener, and the listener refers to the Connection (to send a packet). There are some possible solutions. One way to break cycles is to use weak references. In this way you can observe an object without preventing it from being collected. In the general case, cycles are very bad because you aren't sure which object can be deleted when. However in this case the relationship is simple. Generator can only generate events when it is alive, and Listener will only try to interact with Generator as a response to its event (so we know Generator is alive if we got its event). One could use a weak pointer in the Generator to the Listener, but then the Listener may die as soon as it is registered. The Listener can use a weak pointer to the Generator, and this seems intuitive since the Listener is merely an observer, but accessing a weak pointer safely in a multithreaded environment is expensive (I think so?) when we are positive it exists. The issue is compounded in my library since Generator is an object that I have written (therefore I can trust it to understand all of my "choices"), while the Listener is defined by the user. So I prefer to make the task as easy as possible on the user -- so I want to allow the user to use weak or strong references if possible so he doesn't have to worry about cycles. If I must impose a restriction along the lines of "you can have a pointer to object type x", I will, but I much prefer to not have restrictions like that. One way I've been considering (and have implemented some already) is to have the Generator release all its Listeners when it is done generating events. This works, but in my old API I have ways to retrieve the set listener, and this cannot work since the set listener may go to NULL at any time (as a weak pointer). So my best option for now seems to be to break my old API by allowing the user to set listeners and objects but not be able to retrieve them back (or allow retrieve function knowing that an empty shared_ptr may be returned). Another possible way is for the Generator always to pass a reference to itself in the event. This is possible and clean and very fast, but requires major changes to my API, but the prior way of disallowing gets is safe in that it allows the user to not have to think twice about keeping a reference to my object. I have more ideas but this post is getting long. If you have comments about how something like this using shared_ptr is typically designed, or comments related to my more specific problem, I would be grateful. Thanks in advance, Jason Winnebeck
Jason Winnebeck wrote: [...]
I'm having a problem with trying to mitigate reference cycles in a event generator/event listener paradigm. One creates a listener object, and registers itself as a listener of that object. As a result of the event, it may want to invoke methods of that object, so it contains a reference to that object.
So Generator contains a reference to Listener, and Listener contains a reference to Generator. This is a problem with reference counted pointers.
There are no inherent cycles so far in the design. A typical Listener interface is struct Listener { virtual void accept(Event const & event, Generator & sender) = 0; }; so that it can listen to several Generators at a time. The assumption is that Listener::accept cannot destroy 'sender'. Logically, if a Generator does not own its Listeners, it should keep weak pointers. The advantage is that dead Listeners that haven't unregistered can be auto-unsubscribed by the Generator (unless you have real measurements that indicate a performance problem with weak_ptr::lock(), which would surprise me :-).) HTH
Peter Dimov wrote:
Jason Winnebeck wrote: There are no inherent cycles so far in the design. A typical Listener interface is
struct Listener { virtual void accept(Event const & event, Generator & sender) = 0; };
so that it can listen to several Generators at a time. The assumption is that Listener::accept cannot destroy 'sender'.
Hmm yes I do like that interface. For some reason when I designed my API over 2 years ago I didn't think to pass the generator into the event. I expected that the listener would hold a strong pointer to its generator anyway. Since I'm breaking backwards compatibility anyway for a redesign, one could argue now is the time to break it more, but I think breaking less is better than breaking more... I would be pretty warm to it, but I want to first hear how you would design the user code. I thought it was very convinent to set a listener and just go about ignoring it forever if you want. If you were to say to just simply create a list of listeners you never access or care about in some list simply to keep them alive, removing listeners when their connections died (you would also have to keep a list of all connections and scan them constantly to check for dead connections), then you have just ended up implementing a garbage collection scheme, and one would have to ask why use shared_ptr to begin with when you have to contain lists of every object in your program and scan them for unused objects. I wanted to move the API to use shared_ptr for that explicit reason so that the user doesn't have to maintain lists of every object they create checking for object death.
Logically, if a Generator does not own its Listeners, it should keep weak pointers. The advantage is that dead Listeners that haven't unregistered can be auto-unsubscribed by the Generator (unless you have real measurements that indicate a performance problem with weak_ptr::lock(), which would surprise me :-).)
It makes real sense when you say it that way. Although my generator classes only allow for a single listener (perhaps a bad idea), but I have thought about changing this, or providing a listener that has a list of listeners it distributes across (an adaptor of sorts). I could implement both the single listener/not passing generator reference scheme, and provide an adaptor class that allows for multiple listeners and passing in a reference to the event source, but as said before, adding complexity like that to the API is generally bad. I am curious about the overhead for weak_ptr's lock. Looking at the code you have to do the normal mutex lock then a try/catch and construct a new shared_ptr and so forth. It's the synchronization cost that I'm unsure about but I would think that the code might be a little expensive? I would say events are occuring constantly, but events occuring every 10-100ms is not constant by any means to a computer, and certainly not a "tight loop" subject to optimization. Thank you for your time, Jason
participants (2)
-
Jason Winnebeck
-
Peter Dimov