Могут ли таймеры автоматически получать мусор?

Когда вы используете Timer или Thread, которые будут запускаться только на протяжении всего срока службы программы, вам нужно сохранить ссылку на них, чтобы предотвратить их сбор мусора?

Пожалуйста, оставьте в стороне тот факт, что нижеприведенная программа может иметь Timer как статическую переменную в классе, это просто пример игрушек, чтобы показать проблему.

public class Program
{
    static void Main(string[] args)
    {
        CreateTimer();

        Console.ReadLine();
    }

    private static void CreateTimer()
    {
        var program = new Program();

        var timer = new Timer();
        timer.Elapsed += program.TimerElapsed;
        timer.Interval = 30000;
        timer.AutoReset = false;
        timer.Enabled = true;
    }

    private void TimerElapsed(object sender, ElapsedEventArgs e)
    {
        var timerCast = (Timer)sender;

        Console.WriteLine("Timer fired at in thread {0}", GetCurrentThreadId());

        timerCast.Enabled = true;
    }

    ~Program()
    {
        Console.WriteLine("Program Finalized");
    }

    [DllImport("kernel32.dll")]
    static extern uint GetCurrentThreadId();
}

Может ли таймер получить собранный в этом примере выше? Я запустил его некоторое время, и у меня не было исключения, и не было сообщено сообщение ~Program().

ОБНОВЛЕНИЕ: Я узнал из этот вопрос (спасибо sethcran) что потоки отслеживаются CLR, но мне все же хотелось бы получить ответ о таймерах.

Ответы

Ответ 1

Это только проблема с классом System.Threading.Timer, если вы в противном случае не храните ссылку на него где-то. Он имеет несколько перегрузок конструктора, важные из которых занимают объект состояния. CLR обращает внимание на этот объект состояния. Пока он упоминается где-то, CLR сохраняет таймер в очереди таймера, и объект таймера не будет собирать мусор. Большинство программистов не будут использовать этот объект состояния, статья MSDN, безусловно, не объясняет его роли.

System.Timers.Timer - это оболочка для класса System.Threading.Timer, что упрощает его использование. В частности, он будет использовать этот объект состояния и сохранить ссылку на него, пока включен таймер.

Обратите внимание, что в вашем случае свойство таймера Enabled является ложным, когда оно входит в обработчик события Elapsed, поскольку у вас есть AutoReset = false. Таким образом, таймер имеет право на сбор, как только он входит в ваш обработчик событий. Но вы не попадаете в неприятности, ссылаясь на аргумент отправителя, требуя установить Enabled обратно в true. Из-за чего джиттер сообщает ссылку, поэтому у вас нет проблемы.

Будьте осторожны с обработчиком событий Elapsed. Любое исключение, заброшенное внутри этого метода, будет проглочено без диагностики. Это также означает, что вы не установите Enabled обратно в true. Вы должны использовать try/catch, чтобы сделать что-то разумное. Если вы не намерены заканчивать свою программу, как минимум, вам нужно будет сообщить своей основной программе, что что-то больше не работает. Включение Enabled = true в предложении finally может избежать сбора мусора таймера, но с риском того, что ваша программа будет перебрасывать исключения снова и снова.

Ответ 2

Добавьте этот код в программу и запустите его. Вы увидите, что таймер НЕ собран.

    private void DoStuff()
    {
        CreateTimer();
        Console.WriteLine("Timer started");
        int count = 0;
        for (int x = 0; x < 1000000; ++x)
        {
            string s = new string("just trying to exercise the garbage collector".Reverse().ToArray());
            count += s.Length;
        }
        Console.WriteLine(count);
        Console.Write("Press Enter when done:");
        Console.ReadLine();
    }

    private void Ticktock(object s, System.Timers.ElapsedEventArgs e)
    {
        Console.WriteLine("Ticktock");
    }

    private void CreateTimer()
    {
        System.Timers.Timer t = new System.Timers.Timer(); // Timer(Ticktock, null, 1000, 1000);
        t.Elapsed += Ticktock;
        t.Interval = 1000;
        t.AutoReset = true;
        t.Enabled = true;
    }

Таким образом, ответ на ваш вопрос заключается в том, что таймер не имеет права на сбор и не собирается, если вы не поддерживаете ссылку на него.

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

Ответ 3

Давайте проведем эксперимент:

private static void UnderTest() {
  // Timer is a local varibale; its callback is local as well
  System.Threading.Timer timer = new System.Threading.Timer(
    (s) => { MessageBox.Show("Timer!"); }, 
     null, 
     1000, 
     1000);
}

...

// Let perform Garbage Colelction manually: 
// we don't want any surprises 
// (e.g. system starting collection in the middle of UnderTest() execution)
GC.Collect(2, GCCollectionMode.Forced);

UnderTest();

// To delay garbage collection
// Thread.Sleep(1500);

// To perform Garbage Collection
// GC.Collect(2, GCCollectionMode.Forced);

До сих пор

  • если мы будем комментировать оба Thread.Sleep(1500); и GC.Collect(2, GCCollectionMode.Forced); мы увидим появившиеся сообщения: таймер работает
  • если мы раскомментируем GC.Collect(2, GCCollectionMode.Forced); ничего не увидим: таймер запускается чем он собран
  • если мы раскомментируем оба Thread.Sleep(1500); и GC.Collect(2, GCCollectionMode.Forced); мы увидим одно окно сообщения: таймер запускается, выключается одно окно сообщения, а затем таймер собирается

Так что Timer собраны как любые другие экземпляры объектов.