On the D Blog--Symphony of Destruction: Structs, Classes, and the GC

Petar Petar
Thu Mar 18 12:27:56 UTC 2021


On Thursday, 18 March 2021 at 09:21:27 UTC, Per Nordlöw wrote:
> On Thursday, 4 March 2021 at 13:54:48 UTC, Mike Parker wrote:
>> The blog:
>> https://dlang.org/blog/2021/03/04/symphony-of-destruction-structs-classes-and-the-gc-part-one/
>
> Btw, what is the motive behind D's GC not being able to 
> correctly handle GC allocations in  class destructors.
>
> Is it by design or because of limitations in D's current GC 
> implementation?

Just implementation deficiency. I think it is fixable with some 
refactoring of the GC pipeline. One approach would be, (similar 
to other language implementations - see below), that GC-allocated 
objects with destructors should be placed on a queue and their 
destructors be called when the GC has finished the collection. 
Afterwards, the GC can release their memory during the next 
collection.

> And how does this relate to exception-throwing destructors in 
> other managed languages such as C# and Go; are they forbidden 
> or allowed and safe thanks to a more resilient GC?

TL;DR
* Go doesn't have exceptions or destructors. You can attach a 
finalizer function to an object via [0] which will be called 
before the object will be collected. After the associated 
finalizer is called, the object is marked as reachable again and 
the finalizer function is unset. Since all finalizers are called 
in a separate goroutine, it is not an issue to allocate memory 
from them, as technically this happens separately from the actual 
garbage collection.

* There is something like destructors (aka finalizers) in C#, but 
they can't be used to implement the RAII design pattern. They are 
even less deterministic than destructors of GC-allocated classes 
in D, as they're only called automatically by the runtime and by 
an arbitrary thread. Their runtime designed in such a way that 
memory allocation in destructors is not a problem at all, however 
the default policy is that thrown exceptions terminate the 
process, though that could be configured differently.

---

Instead of destructors, the recommended idiom in Go is to wrap 
resources in wrapper structs and implement a Close() method for 
those types, which the user of the code must not forget to call 
manually and sometimes check for error. They have `defer`, which 
is similar to D's `scope (exit)`. Similar to C#, finalizers in Go 
are not reliable and should probably be only used as a safety net 
to detect whether an object was forgotten to be closed manually. 
If a finalizer takes a long time to complete a clean-up task, it 
is recommended that it spawns a separate goroutine.

---

C# has 2 concepts: finalizers and the IDisposable interface.

C# finalizers [1][2] are defined using the C++ destructor syntax 
`~T()` (rather than D's `~this()`) which is lowered to a method 
that overrides the Object.Finalize() base method like so:

class Resource { ~Resource() { /* custom code */ } } // user code

// gets lowered to:

class Resource
{
   protected override Finalize()
   {
     try { /* custom code */ } finally { base.Finalize(); }
   }
}

Which means that finalization happens automatically from the 
most-derived class to the least derived one. This lowering also 
implies that the implementation is tolerant to exceptions. It is 
an compile-time error to manually define a `Finalize` method. 
Finalizers can only be defined by classes (reference types) and 
not structs (value types). Finalizers are only be called 
automatically (there's no analog to D's `destroy` or C++'s 
`delete`) and the only way to force that is using 
`System.GC.Collect()`, which is almost always bad idea. 
Finalizers used to be called at the end of the application when 
targeting .NET Framework, but the docs say that this is no longer 
the case with the newer the .NET Core, though this may have been 
addressed after the docs were written. The implementation may 
call finalizers from any thread, so your code must be prepared to 
handle that.

Given that finalizers are unsuitable for deterministic resource 
management, it is strongly recommended that class authors should 
implement the IDisposable [3] interface. Users of classes that 
implement IDisposable can either manually call 
IDisposable.Dispose() or they can use the `using` statement [4], 
which is lowered something like this:

using var r1 = new Resource1();
using var r2 = new Resource1();
/* some code */

// vvvvvvvvvvvvvvv

{
     Resource1 r1 = new Resource1();
     try
     {
         {
             Resource2 r2 = expression;
             try
             {
                 /* some code */
             }
             finally
             {
                 if (r2 != null) ((IDisposable)r2).Dispose();
             }
         }
     }
     finally
     {
         if (r1 != null) ((IDisposable)r1).Dispose();
     }
}

IDisposable.Dispose() can be called multiple times (though this 
is discouraged), so your implementation of this interface must be 
able to handle this. Finalizer should call the Dispose() function 
as a safety net.

[0]: https://golang.org/pkg/runtime/#SetFinalizer
[1]: 
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#destructors
[2]: 
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/destructors
[3]: 
https://docs.microsoft.com/en-us/dotnet/api/system.idisposable?view=net-5.0
[4]: 
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement


More information about the Digitalmars-d-announce mailing list