Почему System.Timers.Timer переживает GC, а не System.Threading.Timer? - PullRequest
67 голосов
/ 10 февраля 2011

Похоже, что экземпляры 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 обрабатывается GC: * ​​1011 *

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-кодом, состоит в том, что на не ссылается его обратный вызов.

Добавление явной ссылки на обратный вызов timer1 (например, * 1037)*) достаточно для предотвращения 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();
  }
}

Ответы [ 4 ]

31 голосов
/ 14 февраля 2011

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

8 голосов
/ 27 октября 2013

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

Получается, что 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-only-callback, если обратный вызов ссылается на объект таймера, возвращаемый new, так как в противном случае может быть ошибка гонки.

1 голос
/ 13 февраля 2011

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

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

0 голосов
/ 29 марта 2016

К вашему сведению, начиная с .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 (которая является одноэлементным объектом, поддерживаемым статической переменной-членом также в TimerQueue). В результате все экземпляры таймера будут оставаться активными, пока они активны.

...