Replies: 3 comments 4 replies
-
Great idea to extract this discussion from those issues, and rehash it into a clear best-practice. 👍 When put as above, both "rules" are actually incomplete, and simple examples don't really make clear why you'd need those rules. When fully developed, we'll discover that both rules are almost the same! But first, lets look at the first rule set, and add some examples that are slightly more complicated so that the reasoning behind the rule becomes clear. One thing to keep in mind is that the goal of this rule set is to make it possible to validate each method without any knowledge of what other methods do.
Almost the same as the examples above, but now our method passes the buffer twice. How does this change things? Without any rules, it would depend on what B and C do with the buffer. Imagine both B and C do something with the buffer and release it:
Obviously this would break! A receives the buffer, with a refCount of 1. A passes the buffer to B and B releases the buffer. The buffer now has a refCount of 0 and is thus cleaned up. The buffer is then passed to C and C tries to access the buffer, only to find it in a destroyed state. That is the reason for the first rule in the set: retain it for every call that passes it on.
Now, A receives the buffer, with a refCount of 1. A retains the buffer, putting the refCount at 2. A passes the buffer to B and B releases the buffer, putting the refCount back at 1. A retains the buffer again, putting the refCount at 2, before passing the buffer to C. C releases the buffer and puts the refCount back at 1. A returns now, and the refCount is still at 1.
Now A decreases the refCount before returning, meaning that if the refCount was 1 when A got the buffer, the refCount is 0 before returning it. It also means that A, B and C all behave the same: They reduce the refCount by 1. This loops back to the first rule: retain it for every call that passes it on. Since every method will reduce the refCount by 1, we need to increase it by 1 for every time we pass the buffer to a method. With these two rules, any combination of method calls complete without memory leaks and without reference underflows. And every method can be checked for correctness all by itself, without knowledge of other methods! --> have a look at the second ruleset here? At the start, we mentioned that these rulesets are not complete. Technically the first set is complete, but results in a lot of retaining and releasing. The first set can be improved by adding another rule:
Our last method A has two retains and one release. So we can remove the release and one of the retains... but which one? If we remove the first, then B gets the buffer with a refCount of 1, releases it, and thus destroys the buffer. So removing the first retain is not good. The second one can go though:
Now C gets the buffer with a refCount of 1, and decreases it to 0. Which is fine, since our method A no longer needs the buffer either. Going all the way back to the first example, we can now see that it can also be simplified:
Meaning that the method is the same as for the second rule set... |
Beta Was this translation helpful? Give feedback.
-
Good discussion here. I can appreciate it from an academic perspective. Essentially this boils down to the receiver of a buffer being responsible for releasing it. A receiver has two options for how this can be achieved:
If a buffer is delegated to another receiver, then the sender must assume that the new receiver has followed these rules and released the buffer before returning. This is consistent with the logic above. I'm in agreement that these are good rules. So far this discussion has assumed that a receiver is a method. It doesn't need to be, however. The rules work the same way at any abstraction level (e.g. a class, a queue). The important thing is just that we clearly define those boundaries and that everything within that boundary remains consistent. In an ideal world, we could come up with a set of boundaries that allows us to forget that they exist. Write once and never really have to deal with it again. I think such a boundary exists. Assume we drew the boundary at the I'm curious what both of your thoughts on that are. IMO this makes it largely out of sight and out of mind. The code that deals with references is limited to just a couple places. We can refactor the code as much as we like without needing to care about much at all (including intermediate queues). |
Beta Was this translation helpful? Give feedback.
-
That's a really good way to put it. Putting the boundaries at the method level makes the (practically) smallest containment units. The other extreme would be putting them at the library (complete code-base) level, which makes the largest containment unit. There are many options in between (class, package, etc.) One example of what makes development harder with larger containment units is the inter-unit use of public methods. Lets say we choose the boundaries at class level, and our class has a public method A and a public method B, where A calls B.
In other words, when choosing the class as the containment unit, all public methods can not be used from within the class itself. |
Beta Was this translation helpful? Give feedback.
-
👋 Setting up the context of discussion
Moquette uses Netty and Netty uses reference counting to free the MqttMessages that it decodes and passes to Moquette.
Actually there is some work that (#576 #600) that set up some rules, clearly specified in this comment.
The rules are:
If a method makes or receives a buffer, it must
By sticking to this rule for every method that does anything with buffers, you never have to check what the receiving methods do with the buffer to decide if you need to add a retain or release, making memory leaks a lot easier to find.
And this helped a lot in identification of memory leaks.
Essentially means that an outer method that in case of nesting calls that pass a buffer around, supposing nested calls we have
retain..release
blocks:Then I come to the article https://netty.io/wiki/reference-counted-objects.html#who-destroys-it
which essentially says:
with this rules the code becomes
What do you think about this? could simplify handling? Worth it to think about since it's the Netty's best practice?
cc @hylkevds @jbutler
Beta Was this translation helpful? Give feedback.
All reactions