Gem #100: Reference Counting in Ada - Part 3: Weak References
As we mentioned in the first two parts of this Gem series, GNATCOLL now includes a package that provides support for memory management using reference counting, including taking advantage of the efficient synchronized add-and-fetch intrinsic function on systems where it is available.
There is one thing that reference-counted types cannot handle as well as a full-scale garbage collector: cycles. If A references B which references A, neither of them will ever get freed. A garbage collector is often able to detect such cycles and deallocate all the objects as appropriate, but such a case cannot be handled automatically through reference counting. However, there's a variant approach that can handle such cases with only minor changes in the code.
Let's take an example: you are retrieving values from some container (a database for instance), and want to have a local cache to speed things up. The code would likely be organized as follows:
- Get a reference-counted value from the container. Its counter is 1.
- Put it in the cache for later use. The counter is now 2, since the cache itself owns a reference.
- When you are done using the value in your algorithm, you release the reference you had. Its counter goes down to 1 (the cache still owns the reference).
Because of the cache, the value is never freed from memory. This is not good, since memory usage will only keep increasing.
GNATCOLL provides a solution for this issue, through the use of weak references. This is a standard industry term for a special kind of reference: you have a type that points to the same object as a true reference-counted type would, but that type does not hold a reference. Thus, it does not prevent the counter from reaching 0, and the object from being freed.
When the deallocation occurs, the internal data of the weak reference is reset. Thus, if you retrieve the data stored in the weak reference, you get null, not an erroneous access to some freed memory (which might sooner or later result in a Storage_Error).
If we set up the cache so that it uses weak references, the code becomes:
- Get a reference-counted value from the container. Its counter is 1.
- Put it in the cache, through a weak reference. The counter is still 1.
- When you are done using the value, the counter goes down to 0, and the memory is freed.
- At this point, the cache still contains the weak reference, but the latter uses just a little memory.
Using slightly more complex code, it is possible, in fact, to remove the entry for the cache altogether when the value is freed, thus really releasing all memory to the system. Though GNATCOLL does provide a capability for using weak references, a future package will provide easier handling of such caches.
One way to implement weak references is by adding an extra pointer in type Refcount. GNATCOLL chooses to make this optional: if you want to systematically have that extra pointer in your data structure, you can use weak references. Otherwise, you still have access to the code we described in the first part of this Gem series.
We will not go into the details of the implementation for a weak reference. Interested parties can look at the code in GNATCOLL.Refcount.Weakref, which is relatively small.