SoFunction
Updated on 2025-03-07

Detailed explanation of the timer class and its garbage collection mechanism in C#

About C# Timer class There are 3 timer classes in C#

Methods used by C# Timer 1. Defined in

Method 2 used by C# Timer. Defined in class. "

Methods used by C# Timer 3. Defined in class

Let's take a look at the explanation of these 3 C# Timer usages:

(1)

Applied in WinForm, it is implemented through the Windows message mechanism, similar to the Timer control in VB or Delphi, and is implemented internally using the API SetTimer. Its main disadvantage is that the timing is inaccurate and there must be a message loop, which the Console Application cannot be used.
 
(2)

Very similar, they are implemented through .NET  Thread  Pool, lightweight, precise timing, and have no special requirements for applications and messages.

(3) It can also be applied to WinForm, completely replacing the Timer control above. The disadvantage is that they do not support direct drag and drop and require manual encoding.

C# Timer usage example

Use class

 t =  
new (10000); 
//Instantiate the Timer class and set the interval to 10000 milliseconds; +=  
new (theout); 
//Execute the event when the time arrives; = true; 
//Set whether to execute once (false) or always (true); = true; 
//Whether the event is executed; 
public void theout( 
object source,  
 e)  
 {  
  ("OK!");  
 } 

 
Timer's garbage collection mechanism
Usually when we need to execute a task regularly, we need a timer. At this time, we can use the Timer timer in the c# space; it is an asynchronous timer. When the time comes, a thread will be allocated in the thread pool to execute the task. Let's take a look at an interesting example:

class Program
  {
    static void Main(string[] args)
    {
      Timer timer = new Timer(TimerCallback,null,0,2000);
      
      ();
    }
 
    private static void TimerCallback(object o)
    {
      ("in TimerCallback method");
      ();
 
      
    }
  }

When we run this program in debug mode, as we expect, the program will execute the method every 2 seconds, printing out "in TimerCallback method", and when executed in release mode, the method is only executed once and the string is only printed once. Here, when we call the TimerCallback method, we enforce the garbage collector. This means that in the release mode, when the garbage collector executes the recycling algorithm, it first assumes that all objects are recyclable. After assigning the Timer object to the variable t, t is not referenced, so no variable refers to the Timer object, so the Timer object will be recycled at this time. So why can it run in debug mode? This is related to the optimization method of the c# compiler. The compiler does related optimization operations in release mode. In debug mode, the generation period of the timer object is the end of the method, and this is also for the convenience of debugging. Otherwise, when debugging, when we execute Timer timer = new Timer() and want to see the value of timer, it has been recycled by the garbage collector. This is the result we do not expect to see. How the compiler handles it. We can see the IL comparison generated by the compiler after compiling the above code in release mode and debug mode. We know the result.

The IL generated by release mode:

.method private hidebysig static void Main(string[] args) cil managed
{
 .entrypoint
 // Code size    32 (0x20)
 .maxstack 8
 IL_0000: ldnull
 IL_0001: ldftn   void ::TimerCallback(object)
 IL_0007: newobj   instance void [mscorlib]::.ctor(object,
                                           native int)
 IL_000c: ldnull
 IL_000d: ldc.i4.0
 IL_000e: ldc.i4   0x7d0
 IL_0013: newobj   instance void [mscorlib]::.ctor(class [mscorlib],
                                       object,
                                       int32,
                                       int32)
 IL_0018: pop
 IL_0019: call    string [mscorlib]::ReadLine()
 IL_001e: pop
 IL_001f: ret
} // end of method Program::Main

IL generated in debug mode:

method private hidebysig static void Main(string[] args) cil managed
{
 .entrypoint
 // Code size    33 (0x21)
 .maxstack 4
 .locals init ([0] class [mscorlib] timer)
 IL_0000: nop
 IL_0001: ldnull
 IL_0002: ldftn   void ::TimerCallback(object)
 IL_0008: newobj   instance void [mscorlib]::.ctor(object,
                                           native int)
 IL_000d: ldnull
 IL_000e: ldc.i4.0
 IL_000f: ldc.i4   0x7d0
 IL_0014: newobj   instance void [mscorlib]::.ctor(class [mscorlib],
                                       object,
                                       int32,
                                       int32)
 IL_0019: stloc.0
 IL_001a: call    string [mscorlib]::ReadLine()
 IL_001f: pop
 IL_0020: ret
} // end of method Program::Main

From the generated IL, we can see that in debug mode, the generated IL has 19 lines of IL instruction code with red fonts than in release mode. The function of this instruction code is to store the variables on the stack of the 15 lines of the reference Timer object in the local variable 0. Therefore, in debug mode, the t is still referenced and the Timer object cannot be recycled, so the results we expect can also appear. So how can we get the results we expect in both modes? We can do the following.

Correct code:

class Program
  {
    static void Main(string[] args)
    {
      Timer timer = new Timer(TimerCallback,null,0,2000);
    
      ();
      ();
    }

    private static void TimerCallback(object o)
    {
      ("in TimerCallback method");

      ();

      
    }
  }

At this time, whether in release mode or debug mode, our callback method will be called every 2 seconds.