📜 ⬆️ ⬇️

Interesting moments in C # Part 2

In this article, I would like to talk about some features of the representation of objects in memory in .Net, optimizations performed by compilers, and continue the tradition of comrade mynameco , who wrote this article

This post is not focused on coolers, so if you know that using compiles with the Dispose invocation for enumerator, it is not necessary to use interfaces for the foreach operator, but rather to have the GetEnumerator method that returns the enumerator of the correct signature, that Enumerator itself is mutable (mutable ) a structure that can cause an unexpected bug ... Then please do not enter and not read, save your time, do not leave posts like “KG \ AM”, “dujan”, and “Captains otak”. The rest I ask for cat.


')

.Net memory management



In general, a lot has been written on this topic, so we’ll dwell on this briefly:

when the application starts, it creates the so-called GC roots. Roots are needed to build an object graph that looks like this:

image

As a rule, it is built in a separate thread, parallel to the execution thread. In addition, this thread marks objects that are unreachable from the roots of the GC (if the object is not referenced from the roots at the moment, then no one can refer to it in the future) as “to be deleted” (picture 9 and 10 in the picture). When the memory occupied by the program exceeds a certain limit, or the GC.Collect method is called, the actual garbage collection begins.

more can be found in this article .

Why all this?


But to what. The fact is that
An object can be deleted before the method that this object calls is completed.

Suppose we have such a simple class:

public class SomeClass { public int I; public SomeClass(int input) { I = input; Console.WriteLine("I = {0}", I); } ~SomeClass() { Console.WriteLine("deleted"); } public void Foo() { Thread.Sleep(1000); Console.WriteLine("Foo"); } } 


and we will call it this way:

 public class Program { private static void Main() { new Thread(() => { Thread.Sleep(100); GC.Collect(); }) {IsBackground = true}.Start(); new SomeClass(10).Foo(); } }} 


The thread is needed so that the collector deletes the object immediately after it is possible. In reality, the GC, of ​​course, is better not to touch it, the heuristics on which it works have been developed by very intelligent people, and the manual calling of the GC.Collect method only hurts and indicates the problems of architecture.

Run this program and get this output (you need to compile in release mode to allow the compiler to optimize):
image
In reality, of course, it’s difficult to get this, the CLR (well, or DLR) should decide to build it at the exact moment of this method, but according to the specification it is quite possible!

This works because the methods of the instances are no different from the static methods, except that it passes a hidden parameter — this reference, which is no better than the other method parameters. Well, there are still some minor differences from the point of view of CIL (instance methods are always invoked using callvirt , even those that are not marked as virtual when a simple call is used for static methods),

Destructor Features

Destructors in C #: truth or myth?

Yes and no. On the one hand, they are very similar in appearance to C ++ destructors (and even written with the same tilde icon), but in fact we override the Finalize method inherited from the Object class (by the way, note that the destructor does not have public modifiers , private or some other). Therefore, destructors in C # are often called finalizers.

A question from the audience: why bother with Dispose, if in C # there are such excellent tools that are in no way inferior to C ++ destructors?

Armenian radio answers: the problem is in the collector. The fact is that due to the presence of a finalizer, the object must be removed in two passes: first, the finalizer runs (in a separate thread), and only at the next garbage collection will it be possible to free the memory occupied by the object. Because of this, the objects are characterized by the so-called "midlife crisis" - when a lot of objects fall into the second (last) generation, as a result, the collector begins to slow down very much, because instead of the standard cleanup of only zero generation, he is forced to view the entire memory. The explanation does not claim to be complete, but according to the link above, these questions are pretty well disassembled, and long sheets with captain's revelations are rarely someone who likes to read.

Therefore, it is good practice to call the Dispose method on all objects implementing the IDisposable interface. Although, most of the classes of the standard library just in case have a finalizer (“fool-proof”) which looks like this:

  public virtual void Close() //    Stream { this.Dispose(true); GC.SuppressFinalize((object) this); } public void Dispose() { this.Close(); } /// <summary> /// ,        ,     FileStream. /// </summary> [SecuritySafeCritical] ~FileStream() { if (this._handle == null) return; this.Dispose(false); } 


And why all this?

And now, armed with knowledge about the device of finalizers, we can fully “resurrect” objects!

Rewrite the finalizer like this:
  ~SomeClass() { Console.WriteLine("deleted"); Program.SomeClassInstance = this; GC.ReRegisterForFinalize(this); } 


and change the Program a little
  public class Program { public static SomeClass SomeClassInstance; private static void Main() { new Thread(() => { Thread.Sleep(100); GC.Collect(); }) {IsBackground = true}.Start(); var wr = new WeakReference(new SomeClass(10)); Console.WriteLine("IsAlive = {0}", wr.IsAlive); ((SomeClass)wr.Target).Foo(); Console.WriteLine("IsAlive = {0}", wr.IsAlive); } } 


Now, after the launch, we will get an infinitely printed line of deleted, because the object will be permanently deleted, resurrected, deleted again, and so on to infinity ... (A bit like Skeleton King'a with aegis for 3 charges :))

image

Finalizer may never be called


Well, the simplest thing is if the program is closed forcibly through the task manager or in another non-kosher manner (another plus in the IDisposable box). But in what other cases will it not be called?
Answer
For example, if the program is run for the first time and the objects are not deleted, instead of the finalizer method there will be a stub method (which causes the compilation of the main body of the method). Then, if one day the system doesn’t have enough memory and it collapses with collapse, then unlike other similar cases, the finalizer will not be called simply because JIT has no memory to put the code of this finalizer instead of the stub . In addition, the finalizer of each object can run no more than two seconds, and the entire finalization queue is given no more than 40 seconds.

Add
As noted below correctly, you can inherit from CriticalFinalizerObject. But since there is no multiple inheritance in C # (interfaces are “implemented” and not inherited), this is not a panacea. Although, of course, a similar situation is the rarest of cases, unless there is a memory leak, but we consider from the point of view of the theory whether it is possible or impossible in principle.


Instead of conclusion



On the one hand, you need to summarize all this, on the other - everything seems to have already been told. On the one hand I tried to somehow write about interesting opportunities, on the other hand, I didn’t think much about what is already very well written in other articles. What happened, you decide.

A little advice on using finalizers:


Well, as comrade mayorovp correctly said
Any unmanaged resource must be “wrapped” in SafeHandle or in CriticalHandle. This will eliminate the problems with unloading the AppDomain during the initialization of the unmanaged resource.

And classes that use unmanaged resources through secure wrappers do not need finalizers - after all, finalizers already exist in these wrappers.


PS Any comments on semantic / spelling / punctuation / syntactic / semantic / morphological and other errors are accepted.

Source: https://habr.com/ru/post/211404/


All Articles