Нерентабельные таймеры

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

Могу ли я настроить это поведение, когда создаю таймер? (Я не против, какой .NET-таймер я использую, я просто хочу, чтобы он был потокобезопасным).

Я знаю, что могу реализовать блокировки внутри моей функции обратного вызова, но я думаю, что было бы более элегантно, если бы оно находилось на уровне таймера.

Моя функция обратного вызова и среда не связаны с пользовательским интерфейсом.

[Изменить 1] Я просто не хочу, чтобы в моей функции обратного вызова было более одного потока.

[Редактировать 2] Я хочу сохранить блокировку внутри уровня таймера, потому что таймер отвечает за вызов моего обратного вызова, и здесь возникает особая ситуация, когда я не хочу вызывать свою функцию обратного вызова. Поэтому я думаю, что , когда звонить - это таймер.

Ответ 1

Я предполагаю, что, поскольку ваш вопрос не совсем ясен, вы хотите, чтобы ваш таймер не мог повторно ввести ваш обратный вызов, пока вы обрабатываете обратный вызов, и вы хотите сделать это без блокировки. Вы можете достичь этого с помощью System.Timers.Timer и убедиться, что для свойства AutoReset установлено значение false. Это гарантирует, что вы должны запускать таймер на каждом интервале вручную, тем самым предотвращая любое повторное включение:

public class NoLockTimer : IDisposable
{
    private readonly Timer _timer;

    public NoLockTimer()
    {
        _timer = new Timer { AutoReset = false, Interval = 1000 };

        _timer.Elapsed += delegate
        {
            //Do some stuff

            _timer.Start(); // <- Manual restart.
        };

        _timer.Start();
    }

    public void Dispose()
    {
        if (_timer != null)
        {
            _timer.Dispose();
        }
    }
} 

Ответ 2

Дополняя решение Tim Lloyd для System.Timers.Timer, вот решение для предотвращения повторного размещения в случаях, когда вы хотите использовать System.Threading.Timer.

TimeSpan DISABLED_TIME_SPAN = TimeSpan.FromMilliseconds(-1);

TimeSpan interval = TimeSpan.FromSeconds(1);
Timer timer = null; // assign null so we can access it inside the lambda

timer = new Timer(callback: state =>
{
  doSomeWork();
  try
  {
    timer.Change(interval, DISABLED_TIME_SPAN);
  }
  catch (ObjectDisposedException timerHasBeenDisposed)
  {
  }
}, state: null, dueTime: interval, period: DISABLED_TIME_SPAN);

Я считаю, что вы не хотите, чтобы interval был доступен внутри обратного вызова, но это легко исправить, если вы хотите: Поместите вышеуказанное в класс NonReentrantTimer, который обертывает BCL Timer класс. Затем вы передадите обратный вызов doSomeWork в качестве параметра. Пример такого класса:

public class NonReentrantTimer : IDisposable
{
    private readonly TimerCallback _callback;
    private readonly TimeSpan _period;
    private readonly Timer _timer;

    public NonReentrantTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period)
    {
        _callback = callback;
        _period = period;
        _timer = new Timer(Callback, state, dueTime, DISABLED_TIME_SPAN);
    }

    private void Callback(object state)
    {
        _callback(state);
        try
        {
            _timer.Change(_period, DISABLED_TIME_SPAN);
        }
        catch (ObjectDisposedException timerHasBeenDisposed)
        {
        }
    }


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

Ответ 3

Я знаю, что могу реализовать блокировки внутри моей функции обратного вызова, но я думаю, что это будет более элегантно, если оно будет на уровне таймера

Если требуется блокировка, как это может сделать таймер? Вы ищете волшебную халяву.

Re Edit1:

Ваш выбор - System.Timers.Timer и System.Threading.Timer, оба требуют предосторожности против повторного входа. См. эту страницу и найдите раздел Работа с реестром событий.

Ответ 4

using System;
using System.Diagnostics;
using System.Threading;
using Timer = System.Windows.Forms.Timer;

/// <summary>
///     Updated the code.
/// </summary>
public class NicerFormTimer : IDisposable {

    public void Dispose() {
        using ( this.Timer ) { }
    }

    private Boolean access { get; set; }

    private Timer Timer { get; }

    /// <summary>
    ///     Perform an <paramref name="action" /> after the given interval (in <paramref name="milliseconds" />).
    /// </summary>
    /// <param name="action"></param>
    /// <param name="repeat">Perform the <paramref name="action" /> again. (Restarts the <see cref="Timer" />.)</param>
    /// <param name="milliseconds"></param>
    public NicerFormTimer( Action action, Boolean repeat, Int32? milliseconds = null ) {
        if ( action == null ) {
            return;
        }

        this.Timer = new Timer {
            Interval = milliseconds.GetValueOrDefault( 1 )
        };

        this.Timer.Tick += ( sender, args ) => {
            try {
                if ( Monitor.TryEnter( this.access ) ) {
                    this.access = true;
                    this.Timer.Stop();
                    action.Invoke();
                }
            }
            catch ( Exception exception ) {
                Debug.WriteLine( exception );
            }
            finally {
                if ( this.access ) {
                    Monitor.Exit( this.access );
                    this.access = false;
                }

                if ( repeat ) {
                    this.Timer.Start();
                }
            }
        };

        this.Timer.Start();
    }

}

/// <summary>
///     Updated the code.
/// </summary>
public class NicerSystemTimer : IDisposable {

    public void Dispose() {
        using ( this.Timer ) { }
    }

    private Boolean access { get; set; }

    private System.Timers.Timer Timer { get; }

    /// <summary>
    ///     Perform an <paramref name="action" /> after the given interval (in <paramref name="milliseconds" />).
    /// </summary>
    /// <param name="action"></param>
    /// <param name="repeat">Perform the <paramref name="action" /> again. (Restarts the <see cref="Timer" />.)</param>
    /// <param name="milliseconds"></param>
    public NicerSystemTimer( Action action, Boolean repeat, Double? milliseconds = null ) {
        if ( action == null ) {
            return;
        }

        this.access = false;

        this.Timer = new System.Timers.Timer {
            AutoReset = false, Interval = milliseconds.GetValueOrDefault( 1 )
        };

        this.Timer.Elapsed += ( sender, args ) => {

            try {
                if ( Monitor.TryEnter( this.access ) ) {
                    this.access = true;
                    this.Timer.Stop();
                    action.Invoke();
                }
            }
            catch ( Exception exception ) {
                Debug.WriteLine( exception );
            }
            finally {
                if ( this.access ) {
                    Monitor.Exit( this.access );
                    this.access = false;
                }

                if ( repeat ) {
                    this.Timer.Start();
                }
            }
        };

        this.Timer.Start();
    }

}

Ответ 5

Как таймер мог узнать о ваших общих данных?

Обратный вызов таймера выполняется в потоке ThreadPool. Итак, у вас будет как минимум 2 потока:

  • Ваш основной поток, где таймер создан и запущен;
  • Поток из ThreadPool для запуска обратного вызова.

И вы несете ответственность за правильную работу с вашими общими данными.

Re editits: chibacity - прекрасный пример.