Почему System.Timers.Timer выживает GC, но не System.Threading.Timer?

Похоже, что экземпляры 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, Release, вне отладчика), только System.Threading.Timer имеет GC’ed:

 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 вызов timer1 (например, Console.WriteLine("Stayin alive (" + timer1.GetType().FullName + ")") ) является достаточным для предотвращения GC.

Использование однопараметрического конструктора в System.Threading.Timer также работает, потому что таймер будет ссылаться на него как параметр state . Следующий код сохраняет оба таймера живыми после GC, поскольку каждый из них ссылается на их обратный вызов из таблицы GCHandle :

 class Program { static void Main(string[] args) { System.Threading.Timer timer1 = null; timer1 = new System.Threading.Timer(_ => Console.WriteLine("Stayin alive (1)...")); timer1.Change(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(); } } 

Вы можете ответить на этот и подобные вопросы с помощью 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 )

Я недавно искал эту проблему, посмотрев некоторые примеры реализации Task.Delay и сделав некоторые эксперименты.

Оказывается, независимо от того, зависит ли System.Threading.Timer от GCd, как вы его построите !!!

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

Я нашел это из кода по адресу http://www.dotnetframework.org/default.aspx/DotNET/DotNET/[email protected]/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/[email protected] / 1 / Таймер @ CS

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

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

Добавьте Console.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)... 

Поскольку я смотрю на реализацию System.Threading.Timer , это, похоже, имеет смысл, поскольку кажется, что текущая версия .NET использует связанный список активных объектов таймера, и этот связанный список удерживается переменной-членом внутри TimerQueue (которая является объект singleton, поддерживаемый статической переменной-членом также в TimerQueue). В результате все экземпляры таймера будут оставаться в живых до тех пор, пока они активны.

  • Лучший таймер для использования в службе Windows
  • Пауза / остановка и запуск / возобновление Java TimerTask непрерывно?
  • Перемещение объектов и таймеров
  • Сравнение таймера с DispatcherTimer
  • Как создать таймер пакетного файла для выполнения / вызова другой партии в течение дня
  • Лучший метод синхронизации в C?
  • java: запустить функцию через определенное количество секунд
  • Как использовать class .NET Timer для запуска события в определенное время?
  • Печать «привет мир» каждые X секунд
  • вызов метода через каждые 60 секунд в iPhone
  • Java Animate JLabel
  • Давайте будем гением компьютера.