Зачем нужна система.Таймеры.Таймер выжить GC, но не система.Нарезка резьбы.Таймер?


получается, что System.Timers.Timer экземпляры поддерживаются в живых каким-то механизмом, но System.Threading.Timer экземпляров нет.

пример программы, с периодическим System.Threading.Timer и автосброс System.Timers.Timer:

class Program
{
  static void Main(string[] args)
  {
    var timer1 = new System.Threading.Timer(
      _ => Console.WriteLine("Stayin alive (1)..."),
      null,
      0,
      400);

    var timer2 = new System.Timers.Timer
    {
      Interval = 400,
      AutoReset = true
    };
    timer2.Elapsed += (_, __) => Console.WriteLine("Stayin alive (2)...");
    timer2.Enabled = true;

    System.Threading.Thread.Sleep(2000);

    Console.WriteLine("Invoking GC.Collect...");
    GC.Collect();

    Console.ReadKey();
  }
}

когда я запускаю эту программу (.NET 4.0 Client, Release, вне отладчика), только System.Threading.Timer Это ГК объед:

Stayin alive (1)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Invoking GC.Collect...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...

EDIT: я принял ответ Джона ниже, но я хотел бы изложить его немного.

при запуске образец программа выше (с точкой останова в Sleep), вот состояние рассматриваемых объектов и GCHandle стол:

!dso
OS Thread Id: 0x838 (2104)
ESP/REG  Object   Name
0012F03C 00c2bee4 System.Object[]    (System.String[])
0012F040 00c2bfb0 System.Timers.Timer
0012F17C 00c2bee4 System.Object[]    (System.String[])
0012F184 00c2c034 System.Threading.Timer
0012F3A8 00c2bf30 System.Threading.TimerCallback
0012F3AC 00c2c008 System.Timers.ElapsedEventHandler
0012F3BC 00c2bfb0 System.Timers.Timer
0012F3C0 00c2bfb0 System.Timers.Timer
0012F3C4 00c2bfb0 System.Timers.Timer
0012F3C8 00c2bf50 System.Threading.Timer
0012F3CC 00c2bfb0 System.Timers.Timer
0012F3D0 00c2bfb0 System.Timers.Timer
0012F3D4 00c2bf50 System.Threading.Timer
0012F3D8 00c2bee4 System.Object[]    (System.String[])
0012F4C4 00c2bee4 System.Object[]    (System.String[])
0012F66C 00c2bee4 System.Object[]    (System.String[])
0012F6A0 00c2bee4 System.Object[]    (System.String[])

!gcroot -nostacks 00c2bf50

!gcroot -nostacks 00c2c034
DOMAIN(0015DC38):HANDLE(Strong):9911c0:Root:  00c2c05c(System.Threading._TimerCallback)->
  00c2bfe8(System.Threading.TimerCallback)->
  00c2bfb0(System.Timers.Timer)->
  00c2c034(System.Threading.Timer)

!gchandles
GC Handle Statistics:
Strong Handles:       22
Pinned Handles:       5
Async Pinned Handles: 0
Ref Count Handles:    0
Weak Long Handles:    0
Weak Short Handles:   0
Other Handles:        0
Statistics:
      MT    Count    TotalSize Class Name
7aa132b4        1           12 System.Diagnostics.TraceListenerCollection
79b9f720        1           12 System.Object
79ba1c50        1           28 System.SharedStatics
79ba37a8        1           36 System.Security.PermissionSet
79baa940        2           40 System.Threading._TimerCallback
79b9ff20        1           84 System.ExecutionEngineException
79b9fed4        1           84 System.StackOverflowException
79b9fe88        1           84 System.OutOfMemoryException
79b9fd44        1           84 System.Exception
7aa131b0        2           96 System.Diagnostics.DefaultTraceListener
79ba1000        1          112 System.AppDomain
79ba0104        3          144 System.Threading.Thread
79b9ff6c        2          168 System.Threading.ThreadAbortException
79b56d60        9        17128 System.Object[]
Total 27 objects

как указал Джон в своем ответе, оба таймера регистрируют свой обратный вызов (System.Threading._TimerCallback) в GCHandle таблица. Как отметил Ганс в своем комментарии,state параметр также сохраняется в живых, когда это делается.

как указал Джон, причина System.Timers.Timer сохраняется в живых, потому что на него ссылается обратный вызов (он передается как state параметр для внутреннего System.Threading.Timer); аналогично, причина наша System.Threading.Timer это GC'Ed, потому что это не ссылка на его прослушивание.

добавление явной ссылки на timer1обратный вызов (например,Console.WriteLine("Stayin alive (" + timer1.GetType().FullName + ")")) достаточно для предотвращения GC.

использование конструктора с одним параметром на System.Threading.Timer также работает, потому что таймер будет ссылаться на себя как

4 63

4 ответа:

вы можете ответить на этот и подобные вопросы с windbg, sos и !gcroot

0:008> !gcroot -nostacks 0000000002354160
DOMAIN(00000000002FE6A0):HANDLE(Strong):241320:Root:00000000023541a8(System.Thre
ading._TimerCallback)->
00000000023540c8(System.Threading.TimerCallback)->
0000000002354050(System.Timers.Timer)->
0000000002354160(System.Threading.Timer)
0:008>

в обоих случаях собственный таймер должен предотвратить GC объекта обратного вызова (через GCHandle). Разница в том, что в случае System.Timers.Timer обратный вызов ссылок System.Timers.Timer объект (который реализуется внутренне с помощью System.Threading.Timer)

Я недавно искал эту проблему, посмотрев на некоторые примеры реализации задачи.Задержитесь и сделайте несколько экспериментов.

получается, что будь то система или нет.Нарезка резьбы.Таймер GCd зависит от того, как вы его построите!!!

Если построено только с обратным вызовом, то объект состояния будет сам таймер, и это предотвратит его от GC'D. это, похоже, нигде не документируется, и все же без него это чрезвычайно трудно создать огонь и забыть таймеры.

Я нашел это из кода на http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/Timer@cs/1/Timer@cs

комментарии в этом коде также указывают, почему всегда лучше использовать обратный вызов-только ctor, если обратный вызов ссылается на объект таймера, возвращенный new, поскольку в противном случае может быть ошибка гонки.

в timer1 вы даете ему обратный вызов. В timer2 к вам подключается обработчик событий; это настраивает ссылку на ваш класс программы, что означает, что таймер не будет GCed. Поскольку вы никогда не используете значение timer1 снова, (в основном то же самое, как если бы вы удалили var timer1 = ) компилятор достаточно умен, чтобы оптимизировать переменную. Когда вы нажимаете вызов GC, ничто больше не ссылается на timer1, поэтому его "собрали".

добавить консоль.Writeline после вашего звонка в GC выведите одно из свойств timer1, и вы заметите, что он больше не собирается.

FYI, начиная с .NET 4.6 (если не раньше), это больше не соответствует действительности. Ваша тестовая программа, когда она запускается сегодня, не приводит к тому, что какой-либо таймер собирается мусор.

Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Invoking GC.Collect...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...

Как я смотрю на внедрение системы.Нарезка резьбы.Таймер, это кажется разумным, поскольку кажется, что текущая версия .NET использует связанный список активных объектов таймера, и этот связанный список хранится переменной-членом внутри TimerQueue (который является синглтоном объект, поддерживаемый статической переменной-членом также в TimerQueue). В результате все экземпляры таймера будут оставаться активными до тех пор, пока они активны.