System.Threading.Timer сохранить ссылку на него - PullRequest
2 голосов
/ 19 мая 2010

Согласно [http://msdn.microsoft.com/en-us/library/system.threading.timer.aspx][1] вам необходимо сохранить ссылку на System.Threading.Timer, чтобы предотвратить его удаление.

У меня есть такой метод:

private void Delay(Action action, Int32 ms)
    {
        if (ms <= 0)
        {
            action();
        }

        System.Threading.Timer timer = new System.Threading.Timer(
            (o) => action(), 
            null, 
            ms, 
            System.Threading.Timeout.Infinite);
    }

Что, я думаю, не относится к таймеру, я пока не видел никаких проблем, но это, вероятно, потому, что используемые периоды задержки были довольно малы.

Является ли код выше неправильным? И если это так, как мне сохранить ссылку на таймер? Я думаю, что-то вроде этого может работать:

    class timerstate 
    {
        internal volatile System.Threading.Timer Timer;
    };

    private void Delay2(Action action, Int32 ms)
    {
        if (ms <= 0)
        {
            action();
        }


        timerstate state = new timerstate();
        lock (state)
        {
            state.Timer = new System.Threading.Timer(
                (o) => 
                { 
                    lock (o) 
                    { 
                        action();
                        ((timerstate)o).Timer.Dispose();
                    } 
                },
                state,
                ms,
                System.Threading.Timeout.Infinite);
        }

Дело в блокировке, поэтому я могу включить таймер в класс timerstate до вызова делегата. Все это выглядит немного неуклюжим для меня. Возможно, я должен расценить вероятность срабатывания таймера до того, как он завершит создание и назначен свойству в экземпляре timerstace, как незначительный и оставить блокировку.

Ответы [ 4 ]

1 голос
/ 23 сентября 2018

Код «рабочий» действительно является побочным эффектом недетерминированной сборки мусора / финализаторов .

Этот код, работающий в LINQ Pad как операторы C #, показывает проблему - нет сообщений, которые будут записываться, потому что Timer управляется GC (и вызывается финализатор, который очищает ресурсы внутреннего таймера) ..)

new System.Threading.Timer((o) => { "Hi".Dump(); }, this, 100, 100);
GC.Collect();
Thread.Sleep(2000);

Тем не менее, закомментируйте оператор «GC.Collect», и сообщения будут регистрироваться в течение 2 секунд, поскольку сборщик мусора не [немедленно] выполнен. Финализатор таймера не вызывается до завершения программы.

Поскольку поведение недетерминировано , его также следует считать ошибкой:}

Та же проблема существует в следующем коде, потому что строгая ссылка требуется для гарантии того, что объект не GC-кодирован - в этом примере до сих пор нет ссылки на оболочку timer объект, так что такая же проблема существует, хотя и с еще одним уровнем косвенности ..

1 голос
/ 19 мая 2010

Ваш второй подход также не сохранил бы ссылку. После окончания блока Delay2 ссылка на state исчезает, поэтому сборщик мусора будет собирать ее ... тогда ваша ссылка на Timer также исчезнет, ​​и она будет собрана и утилизирована.

class MyClass
{
    private System.Threading.Timer timer;

    private void Delay(Action action, Int32 ms)   
    {   
        if (ms <= 0)   
        {   
            action();   
        }   

        timer = new System.Threading.Timer(   
            (o) => action(),    
            null,    
            ms,    
            System.Threading.Timeout.Infinite);   
    }   
}
1 голос
/ 20 мая 2010

Я прочитал из ваших комментариев к существующим ответам, что вы можете иметь 0..n действий, и поэтому у вас также будет 0..n таймеров. Это правильно? В этом случае вам следует выполнить одно из следующих действий:

  1. Храните список / словарь таймеров, но в этом случае вы должны удалить таймер после стрельбы.
  2. Создайте планировщик: имейте 1 таймер, который запускается регулярно, для каждого вызова с задержкой добавляйте действие и вычисленное время, когда оно должно попадать в список / словарь, каждый раз при срабатывании таймера проверяйте список, запускайте и удаляйте действие , Вы даже можете построить этот планировщик так, чтобы он сортировал действия по времени выполнения и устанавливал таймер на достаточный интервал.
1 голос
/ 19 мая 2010

Обновление

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

Это в основном то, что вы хотите, чтобы ваш метод делал? Выполнить action через указанное количество миллисекунд? Если это так, я бы предложил что-то вроде следующей альтернативной реализации:

private void Delay(Action action, int ms)
{
    if (ms <= 0)
    {
        action();
        return;
    }

    System.Threading.WaitCallback delayed = state =>
    {
        System.Threading.Thread.Sleep(ms);
        action();
    };

    System.Threading.ThreadPool.QueueUserWorkItem(delayed);
}

... кстати, знаете ли вы, что в размещенном вами коде указание ненулевого значения для ms приведет к тому, что action будет выполнен дважды?


Оригинальный ответ

Класс timerstate действительно не нужен. Просто добавьте член System.Threading.Timer в любой класс, содержащий ваш метод Delay; тогда ваш код должен выглядеть так:

public class Delayer
{
    private System.Threading.Timer _timer;

    private void Delay(Action action, Int32 ms)
    {
        if (ms <= 0)
        {
            action();
        }

        _timer = new System.Threading.Timer(
            (o) => action(), 
            null, 
            ms, 
            System.Threading.Timeout.Infinite);
    }
}

Теперь я вижу, что вы указываете аргумент period конструктора таймера как System.Threading.Timeout.Infinite (-1). Это означает, что вы собираетесь, чтобы таймер вызывал action один раз , после того как ms истекло; я прав? Если это так, то на самом деле нет особой необходимости беспокоиться о том, чтобы таймер был установлен в любом случае (то есть будет, и это нормально), принимая относительно низкое значение для ms.

В любом случае, если вы собираетесь удерживать экземпляр объекта IDisposable (например, System.Threading.Timer), вы, как правило, должны распоряжаться этим членом, когда ваш объект (т. Е. Этот экземпляр) утилизируется. Я полагаю, что у System.Threading.Timer есть финализатор, который в конечном итоге приведет к его удалению, но лучше утилизировать вещи, как только они вам больше не понадобятся. Итак:

public class Delayer : IDisposable
{
    // same code as above, plus...

    public void Dispose()
    {
        _timer.Dispose();
    }
}
...