为什么当Enabled设置为false时,System.Timer.Timer仍会激发事件

本文关键字:Timer 事件 System Enabled 设置 false 为什么 | 更新日期: 2023-09-27 18:00:16

我尝试创建Timer的派生类,该类允许设置"Pause"锁存,以防止工作线程重新激活计时器。但是,当AutoReset设置为false时,将继续引发Elapsed事件,并且一旦设置了Paused变量,Enabled访问器似乎正在执行其工作,以防止基类的Enabled属性被修改。

为什么会发生这种情况,或者我应该使用什么策略来进一步了解这里实际发生了什么互动?

我在下面附上了派生类的实现。

    using System.Timers
    class PauseableTimer : Timer
    {
      public bool Paused;
      new public bool Enabled
      { 
        get
        {
          return base.Enabled;
        } 
        set
        {
          if (Paused)
          {
            if (!value) base.Enabled = false;
          } 
          else
          {
            base.Enabled = value;
          }
        }
      }
    }

举例说明问题。

class Program
{
    private static PauseableTimer _pauseableTimer;
    private static int _elapsedCount;
    static void Main(string[] args)
    {
        _pauseableTimer = new PauseableTimer(){AutoReset = false,Enabled = false,Paused = false};
        _pauseableTimer.Elapsed += pauseableTimer_Elapsed;
        _pauseableTimer.Interval = 1;
        _pauseableTimer.Enabled = true;
        while(_elapsedCount<100)
        {
            if (_elapsedCount > 50) _pauseableTimer.Paused = true;
        }
    }
    static void pauseableTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        Console.WriteLine(String.Format("this.Enabled:'{0}',Paused:'{1}',AutoReset:'{2}",_pauseableTimer.Enabled,_pauseableTimer.Paused,_pauseableTimer.AutoReset));
        _elapsedCount++;
        _pauseableTimer.Interval = _pauseableTimer.Interval == 1 ? 2 : 1; //This line breaks it.
        _pauseableTimer.Enabled = true;
    }
}

为什么当Enabled设置为false时,System.Timer.Timer仍会激发事件

相关文档,System.Timers.Timer.Interval

备注如果Enabled和AutoReset都设置为false,并且计时器以前已启用,则设置Interval属性会引发Elapsed事件一次,就好像Enabled属性已设置为true一样。若要在不引发事件的情况下设置间隔,可以将"自动重置"属性临时设置为true。

建议的将AutoReset设置为true的解决方案并不能解决问题,因为在允许触发事件的事件处理程序期间,存在将AutoReset设置为true这一未记录的行为。

解决方案似乎是构建派生对象,这样就可以避免事件再次触发的许多明显方式中的任何一种。

以下是我结束时的实现。

public class PauseableTimer : Timer
{
    private bool _paused;
    public bool Paused
    {
        get { return _paused; }
        set 
        { 
            Interval = _interval;
            _paused = value;
        }
    }
    new public bool Enabled
    {
        get
        {
            return base.Enabled;
        }
        set
        {
            if (Paused)
            {
                if (!value) base.Enabled = false;
            }
            else
            {
                base.Enabled = value;
            }
        }
    }
    private double _interval;
    new public double Interval
    {
        get { return base.Interval; }
        set
        {
            _interval = value;
            if (Paused){return;}
            if (value>0){base.Interval = _interval;}
        }
    }
    public PauseableTimer():base(1){}
    public PauseableTimer(double interval):base(interval){}
}

恐怕多线程中的一切都更复杂。假设您的代码按您的意愿工作,那么在重置Enabled属性后,可以在一个窗口中引发运行中的事件。请参阅MSDN文档中的这句话。

引发Elapsed事件的信号总是排队等待在上执行ThreadPool线程。这可能会导致在引发的Elapsed事件中在Enabled属性设置为之后false。Stop的代码示例方法显示了一种解决方法这种比赛条件。

另一个选项是抑制事件???我无法解释发生了什么,但下面给出的理论应该可以让你绕过你讨论过的这个小问题。正如Steve所提到的,在您正在尝试设置的"已启用属性上设置观察和断点",并确保它确实正在设置中。

我该如何解决此问题:

捕获并检查"Enabled"属性,并在需要时删除订阅方法(处理程序)的"-=",然后在确实需要处理"Elapsed"事件时再次添加它的"+="。

我已经在一些不同的WinForms项目中多次使用这种风格。如果不希望以编程方式处理"Elapsed"事件,请在满足特定条件时创建一个检查并删除它,然后在满足相反条件时添加它。

if (paused) // determine pause logic to be true in here
{
   timer.Elapsed -= ... // remove the handling method.
}
else
{
   timer.Elapsed += ... // re-add it in again
}

上面的代码逻辑将允许您在"Paused"标志为true时,每次引发"Elapsed"事件时都可以忽略该事件。我希望以上内容能帮助

在System.Timers.Timer中,创建类时会将Elapsed事件添加到ThreadPool中。之后它被发射。此时Enabled属性可能为false。您对此无能为力,但您可以做的是在Elapsed事件触发时测试Enabled属性是否为true。我确实覆盖了Enabled属性来实现这一神奇,作为额外的功能,我还在其中放入了一个IsDisposed属性:

    public class DisposableTimer : System.Timers.Timer {
    /// <summary>
    ///     override the Timer base class Enabled property
    /// </summary>
    /// <remarks>
    ///     the code in the Elapsed event should only be executed when the Enabled property is set to "true".
    ///     we cannot prevent that the Elapsed event is fired at the start, because its automatically put in the ThreadPool,
    ///     but we can prevent that the code in it can be executed when the Enabled property is "false".
    /// </remarks>
    private bool enabled;
    public new bool Enabled 
    {
        get
        {
            return enabled;
        }
        set
        {
            enabled = base.Enabled = value;
        }
    }
    /// <summary>
    ///     count the heartbeats
    /// </summary>
    public int HeartbeatCounter { get; set; }
    /// <summary>
    ///     name of timer
    /// </summary>
    public string TimerName { get; set; }
    /// <summary>
    ///     show heartbeat on console
    /// </summary>
    public bool ShowHeartBeat { get; set; }
    // type of entry in eventlog
    public EventLogEntryType EventLogEntryType { get; set; }
    // updated interval to process messages
    public Func<double> UpdatebleInterval { get; set; }
    /// <summary>
    ///     used eventlog
    /// </summary>
    public EventLog EventLog { get; set; }
    /// <summary>
    ///     message logging 
    /// </summary>
    /// <remarks>
    ///     this property needs to be dynamic because in 
    ///     pda service a different class is used but luckily :-)
    ///     with the same method for adding loggings.
    /// </remarks>
    public dynamic MessageLogging { get; set; }
    /// <summary>
    ///     make sure there are no overlapping set of timer callbacks
    /// </summary>
    private object locker; 
    /// <summary>
    ///     initialize timer class
    /// </summary>
    /// <param name="actions">action to perform</param>
    /// <param name="timerName">name of timer</param>
    public DisposableTimer(List<Action> actions, string timerName) : base() 
    {
        // used to make sure there are no overlapping executing sets of timer callbacks
        locker = new object();
        // wrapper for the actions that need to be performed.
        base.Elapsed += (s, a) => Callback(actions);
        // set name of timer
        this.TimerName = timerName;
        /* 
         * only once a callback is executed after elapsed time,
         * because there is only one callback executed there can be
         * no overlap, because the "reset" is done after the set of
         * callbacks are executed.
         */
        AutoReset = false;
        // timer is not started yet
        Enabled = false;
    }
    /// <summary>
    ///     check if verwijder bericht timer is disposed
    /// </summary>
    public bool IsDisposed
    {
        get
        {
            var timerType = typeof(System.Timers.Timer);
            var timerDisposedField = timerType.GetField("disposed", BindingFlags.NonPublic | BindingFlags.Instance);
            return (bool)timerDisposedField.GetValue(this);
        }
    }
    /// <summary>
    ///     after a callback a timer needs to be reset to continue running if AutoReset=false.
    /// </summary>
    /// <param name="interval">new interval of timer</param>
    private void Reset(double interval)
    {
        // stop the timer
        Stop();
        // only do when not disposed yet.
        if (!IsDisposed)
        {
            // adjust interval if needed
            if (interval != 0)
                Interval = interval;
            // release exclusive lock
            Monitor.Exit(locker);
        }
        // start the timer again
        Start();
    }
    /// <summary>
    ///     only start if not disposed and started yet
    /// </summary>
    public new void Start()
    {
        if (!IsDisposed && !Enabled)
            Enabled = true;
    }
    /// <summary>
    ///     only stop if not disposed and stopped yet
    /// </summary>
    public new void Stop()
    {
        if (!IsDisposed && Enabled)
            Enabled = false;
    }
    /// <summary>
    ///     set of callbacks to perform after timer elapse interval
    /// </summary>
    /// <param name="callBackActions">list of callbacks inside this wrapper to execute</param>
    public void Callback(List<Action> callBackActions)
    {
        // only execute callbacks if timer is enabled.
        if (Enabled)
        {
            /*
             * AutoReset = false, so a callback is only executed once,
             * because of this overlapping callbacks should not occur,
             * but to be sure exclusive locking is also used.
             */
            var hasLock = false;
            // show heartbeat at output window
            if (ShowHeartBeat)
                Debug.WriteLine(string.Format("HeartBeat interval: {0}...{1}/thread: 0x{2:X4}", TimerName, ++HeartbeatCounter, AppDomain.GetCurrentThreadId() ));
            // execute callback action.
            try
            {
                // only perform set of actions if not executing already on this thread.
                Monitor.TryEnter(locker, ref hasLock);
                if (hasLock)
                {
                    // show heartbeat at output window
                    if (ShowHeartBeat)
                        Debug.WriteLine(string.Format("            action: {0}...{1}/thread: 0x{2:X4}", TimerName, HeartbeatCounter, AppDomain.GetCurrentThreadId()));
                    // execute the set of callback actions
                    foreach (Action callBackAction in callBackActions)
                    {
                        // execute callback 
                        try
                        {
                            callBackAction();
                        }
                        // log error, but keep the action loop going.
                        catch (Exception ex)
                        {
                            EventLog.WriteEntry(ex.Message, EventLogEntryType.Warning);
                            MessageLogging.Insert(ex.Message);
                        }
                    }
                }
                // show that action is busy
                else if (ShowHeartBeat)
                    Debug.WriteLine(string.Format("              busy: {0}...{1}/thread: 0x{2:X4}", TimerName, HeartbeatCounter, AppDomain.GetCurrentThreadId()));
            }
            // adjust interval when needed and release exclusive lock when done.
            finally
            {
                // after the complete action is finished the lock should be released.
                if (hasLock)
                {
                    // timer interval can be changed when timer is active, callback function is needed for this.
                    double newInterval = 0;
                    if (UpdatebleInterval != null)
                    {
                        // calculate new interval for timer
                        double updatedInterval = UpdatebleInterval();
                        if (Interval != updatedInterval)
                        {
                            // based on Dutch
                            var dutchCultureInfo = new CultureInfo("nl-NL", false).TextInfo;
                            // write interval change to loggings
                            string intervalMessage = dutchCultureInfo.ToTitleCase(string.Format(@"{0} interval veranderd van {1} naar {2} seconden", TimerName, Interval / 1000, updatedInterval / 1000));
                            EventLog.WriteEntry(intervalMessage, EventLogEntryType.Information);
                            MessageLogging.Insert(intervalMessage);
                            // set for new interval
                            newInterval = updatedInterval;
                        }
                    }
                    // make ready for new callback after elapsed time, lock can be released by now.
                    Reset(newInterval);
                }
            }
        }
        // show heartbeat at output window
        else if (ShowHeartBeat)
            Debug.WriteLine(string.Format("HeartBeat thread: {0}...{1}/thread: 0x{2:X4}", TimerName, ++HeartbeatCounter, AppDomain.GetCurrentThreadId()));
    }
}

我会重新格式化您的代码:

// from this
if (!value) base.Enabled = false;
// to this
if (!value) 
    base.Enabled = false;

它不仅读起来更好,你还可以在关键行上放一个断点,看看它是否正在执行