Чому System.Timers.Timer переживає GC, але не System.Threading.Timer?


76

Здається, 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.TimerGC'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.TimerGC'ed полягає в тому, що на нього не посилається зворотний виклик.

Додавання явного посилання на 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();
  }
}

1
Чому timer1навіть збирають сміття? Хіба це все ще не в обсязі?
Jeff Sternal

3
Джефф: Сфера застосування насправді не актуальна. Це в значній мірі основний сенс для методу GC.KeepAlive. Якщо вас цікавлять прискіпливі деталі, див. Blogs.msdn.com/b/cbrumme/archive/2003/04/19/51365.aspx .
Ніколь Калінойу

7
Погляньте за допомогою Reflector на таймер. Зверніть увагу на хитрість, яку він використовує з "cookie", щоб надати системному таймеру об'єкт стану для використання у зворотному виклику. CLR це відомо, clr / src / vm / comthreadpool.cpp, CorCreateTimer () у вихідному коді SSCLI20. MakeDelegateInfo () ускладнюється.
Ганс Пасант,

1
@Jeff: Це очікувана поведінка; деталі в CLR через C # і згадуються в моєму блозі в розділі "Коли компілятор JIT поводиться інакше (налагодження)".
Стівен Клірі

2
@StephenCleary вау - ментально. Я щойно знайшов помилку в додатку, який обертається навколо System.Timers.Timer залишається в живих і публікує оновлення після того, як я очікував, що він помре. Дякуємо, що заощадили багато часу!
доктор Ендрю Бернетт-Томпсон

Відповіді:


33

Ви можете відповісти на ці та подібні запитання за допомогою 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об’єкт (який реалізований внутрішньо за допомогою a System.Threading.Timer)


11

Нещодавно я гуглив цю проблему, переглянувши деякі приклади реалізації 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 / Таймер @ cs

Коментарі в цьому коді також вказують, чому завжди краще використовувати ctor тільки для зворотного виклику, якщо зворотний виклик посилається на об'єкт таймера, повернутий new, оскільки в іншому випадку може бути расова помилка.


Це насправді відповідає документації. Якщо ви зберігаєте посилання на System.Threading.Timer у зворотному виклику (через закриття або конструктор), тоді він не буде зібраний як будь-який керований об'єкт.
Джо

1

У timer1 ви надаєте йому зворотний дзвінок. У timer2 до вас підключається обробник подій; це налаштовує посилання на ваш клас Програми, що означає, що таймер не буде GCed. Оскільки ви більше ніколи не використовуєте значення timer1 ((в основному те саме, що якщо ви видалили var timer1 =), компілятор досить розумний, щоб оптимізувати змінну. Коли ви натискаєте виклик GC, ніщо вже не посилається на timer1, тому його 'зібрано.

Додайте Console.Writeline після виклику GC, щоб вивести одне з властивостей timer1, і ви помітите, що він більше не збирається.


3
Обробник подій не має посилання на Programклас, і навіть якщо він є, це не завадить таймеру бути GC'ed.
Стівен Клірі

3
Я це робить. Складіть наведений вище код, а потім подивіться на нього за допомогою рефлектора .Net. + = Lamba перетворюється на метод у класі Program. І так, обробники подій, які пов’язані, ЗАПОБІГАТЬ збір сміття. blogs.msdn.com/b/abhinaba/archive/2009/05/05/…
Енді

0

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


2
Я все ще бачу, як System.Threading.Timerекземпляр збирається у .NET 4.6. Переконайтесь, що ви складаєте код у режимі випуску з увімкненими оптимізаціями. Згаданий зв’язаний список містить допоміжні TimerQueueTimerоб’єкти; це не заважає System.Threading.TimerGC збирати оригінальний екземпляр. (Кожен System.Threading.Timerекземпляр посилається на власний TimerQueueTimerоб'єкт, але не навпаки. Коли System.Threading.Timerфайл збирається GC, його TimerQueueTimerоб'єкт видаляється з черги ~TimerHolderфіналізатором.)
Антоша,

Спробуйте в режимі звільнення.
Євген Найда

Те саме тут, запуск коду на .NET Core 3.0. Жоден з двох таймерів не збирає сміття, ні у налагодженні, ні у звіті. Щоб відтворити проблему, я повинен перемістити створення таймерів за межі Mainметоду.
Теодор Zoulias
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.