Why does System.Timer.Timer still fire events when Enabled is set to false?

前端 未结 5 549
佛祖请我去吃肉
佛祖请我去吃肉 2021-01-12 04:35

I have attempted to create a derived class of Timer that allows for a \'Pause\' latch to be set to keep the worker thread from reactivating the timer. However, Elapsed event

相关标签:
5条回答
  • 2021-01-12 04:59

    Everything is more complex in multithreading, I'm afraid. Assuming your code is working as you wish, there is a window where in-flight events can get raised after you reset the Enabled property. See this quote from the MSDN docs.

    The signal to raise the Elapsed event is always queued for execution on a ThreadPool thread. This might result in the Elapsed event being raised after the Enabled property is set to false. The code example for the Stop method shows one way to work around this race condition.

    0 讨论(0)
  • 2021-01-12 05:03

    In System.Timers.Timer the Elapsed event is added to the ThreadPool when the class is created. After that it is fired. The Enabled property can be false at that time. You can't do anything about that, but what you can do is test if the Enabled property is true when the Elapsed event fires. I do override the Enabled property to make this magic happening, as extra I also put an IsDisposed property in it:

        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()));
        }
    }
    
    0 讨论(0)
  • 2021-01-12 05:20

    I would reformat your code:

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

    Not only does it read better, you can put a break point on the key line and see if it's being executed

    0 讨论(0)
  • 2021-01-12 05:23

    Another option is to suppress the event??? I can't explain what is going but the theory presented below should allow you to circumvent this little problem you have discussed. As Steve mentioned put a 'Watch and break point on the enabled property' that you are try set and make sure it is actually being set.

    How would I tackle this:

    Catch and check for the 'Enabled' property and remove '-=' the subscribing method (handler) as of when needed and then re-add '+=' it again when you do need handle the 'Elapsed' event.

    I have used this style quite a few times on a few different WinForms project. If you don't want the 'Elapsed' event to be handled programmatically create a check for and remove it when a certain condition is met and then add it when the opposite condition is met.

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

    The above code logic will allow you code to ignore the 'Elapsed' event ever time it is raised whilst the 'Paused' flag is true. I hope the above helps

    0 讨论(0)
  • 2021-01-12 05:25

    Relevant document, System.Timers.Timer.Interval

    Note If Enabled and AutoReset are both set to false, and the timer has previously been enabled, setting the Interval property causes the Elapsed event to be raised once, as if the Enabled property had been set to true. To set the interval without raising the event, you can temporarily set the AutoReset property to true.

    The recommended solution of setting AutoReset to true does not solve the problem because there is an undocumented behavior of setting AutoReset to true during an event handler also allowing for an event to be fired.

    The solution seems to be to build out the derived object to the point where you can keep any of the apparently many ways that an event can be fired again from happening.

    Below is the implementation that I ended with.

    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){}
    }
    
    0 讨论(0)
提交回复
热议问题