5
\$\begingroup\$

I came up with the idea of a small utility class that will poll some delegate until the response received meets some condition, upon which it will notify the main thread which can take the appropriate action.

This is the first time I've ever done something like this, and I know that in general, polling is a bad idea, so I want to do this in a way that consumes minimal resources, and is really easy to use.

using System;
using System.Threading;
using System.Windows.Threading;

namespace WPF.Utilities.Common
{
    /// <summary>This class allows a user to easily set up a seperate thread to poll some state,
    /// and set up an event that will fire if the state meets some condition.</summary>
    /// <typeparam name="T">The type of the value returned by the polling delegate.</typeparam>
    public class ConditionMonitor<T>
    {
        #region Private Properties
        protected bool Halted = false;
        protected Dispatcher originThread = null;
        protected Thread monitorThread = null;
        #endregion

        #region Delegates
        /// <summary>A delegate provided by the user of this class which returns the current state,
        /// to be tested against a certain condition in the IsConditionMet delegate.</summary>
        public delegate T RequestState();
        public RequestState RequestStateDelegate { get; set; }

        /// <summary>A delegate provided by the user of this class which determines whether given the
        /// current state, the polling program should execute the ConditionMet delegate.</summary>
        public delegate bool IsConditionMet<T>(T state);
        public IsConditionMet<T> IsConditionMetDelegate { get; set; }

        /// <summary>A delegate used to handle ConditionMonitor events.</summary>
        public delegate void ConditionMonitorHandler<T>(ConditionMonitor<T> source, T state);
        /// <summary>An event which fires each time the state is polled (use sparingly).</summary>
        public event ConditionMonitorHandler<T> RequestReceived;
        /// <summary>An event which fires when the condition is met.</summary>
        public event ConditionMonitorHandler<T> ConditionMet;

        /// <summary>A delegate used to handle ConditionMonitor events.</summary>
        public delegate void ConditionMonitorExceptionHandler<T>(ConditionMonitor<T> source, T state, Exception ex);
        /// <summary>An event which fires if an exception is thrown while retrieving the state
        /// or testing whether the condition is met.</summary>
        public event ConditionMonitorExceptionHandler<T> RequestError;
        #endregion

        #region Public Properties
        /// <summary>The time between requests made to the RequestStateDelegate. Default is 1 second (1000ms)</summary>
        public int PollInterval_Milliseconds { get; set; }
        /// <summary>Set to true to automatically halt polling once the condition is met. Default is False.</summary>
        public bool HaltWhenConditionMet { get; set; }
        #endregion

        #region Constructors
        /// <summary>Creates a new instance of a ConditionMonitor</summary>
        public ConditionMonitor()
        {
            originThread = Dispatcher.CurrentDispatcher;
            PollInterval_Milliseconds = (int)TimeSpan.FromSeconds(1).TotalMilliseconds;
            HaltWhenConditionMet = false;
        }
        #endregion

        #region Public Methods
        /// <summary>Begins polling the RequestStateDelegate on a seperate thread.</summary>
        public virtual void BeginMonitoring()
        {
            if( monitorThread != null ) throw new Exception("Previous monitoring has not yet been stopped!");
            if( RequestStateDelegate == null ) throw new Exception("No delegate specified for polling - please set the RequestStateDelegate property.");

            Halted = false;
            monitorThread = new Thread(new ThreadStart(PollState));
            monitorThread.Start();
        }
        /// <summary>Halts polling and ensures that no more requests will be made or events fired.</summary>
        public virtual void StopMonitoring()
        {
            Halted = true;
        }
        #endregion

        #region Private Methods
        /// <summary>Responsible for the polling loop and invoking events back on the origin thread.</summary>
        protected virtual void PollState()
        {
            while( !Halted )
            {
                T state = default(T);
                bool bConditionMet = false;
                try
                {
                    state = RequestStateDelegate();
                    InvokeEvent(RequestReceived, state);
                    if( IsConditionMetDelegate != null && !Halted )
                    {
                        bConditionMet = IsConditionMetDelegate(state);
                        if( bConditionMet )
                        {
                            InvokeEvent(ConditionMet, state);
                            if( HaltWhenConditionMet ) Halted = true;
                        }
                    }
                }
                catch( Exception ex )
                {
                    InvokeExceptionHandler(state, ex);
                }
                if( !Halted ) Thread.Sleep(PollInterval_Milliseconds);
            }
            monitorThread = null;
        }

        /// <summary>Invokes a delegate of type ConditionMonitorHandler on the origin thread.</summary>
        /// <param name="toInvoke">The delegate to invoke (RequestRecieved or ConditionMet)</param>
        /// <param name="state">The response from the last call to the RequestStateDelegate</param>
        protected void InvokeEvent(ConditionMonitorHandler<T> toInvoke, T state)
        {
            if(toInvoke != null && !Halted)
                originThread.BeginInvoke(toInvoke, new object[] { this, state });
        }
        /// <summary>Invokes the exception delegate on the origin thread.</summary>
        /// <param name="state">The response from the last call to the RequestStateDelegate, or null.</param>
        /// <param name="ex">The exception raised while calling the RequestStateDelegate or IsConditionMetDelegate.</param>
        protected void InvokeExceptionHandler(T state, Exception ex)
        {
            if( RequestError != null && !Halted )
                originThread.BeginInvoke(RequestError, new object[] { this, state, ex });
        }
        #endregion
    }
}

My intended first use is going to be in a program that people in my company have running pretty much all the time. They will be working on some 'deal', and want to know if anything associated with that deal changes so they can grab the latest numbers. My RequestStateDelegate will be a small database call to check the last modified time of the entry they're working on. I want the thread to silently poll and ONLY bother the main thread when the modified time is not equal to the modified time of the entry the user has loaded up (the IsConditionMetDelegate will be a small method that does that check).

I just want to be sure I haven't set this up in a way where I risk spinning off more than a single thread, or end up with a thread that keeps running even after StopMonitoring has been called. Is there anything else I need to do to clean up the thread, or is simply letting PollState return sufficient?

I'm also worried about what happens if someone abandons the ConditionMonitor instance without remembering to call StopMonitoring(). The GC would dispose of the class, but what would happen to the thread is spun up - would it get silently aborted, or throw a huge fuss since it references ConditionMonitor properties? I thought I might have to do something like this:

public ~ConditionMonitor()
{
    StopMonitoring();
}

(Or something similar using IDisposable) But will the garbage collector even consider this instance for clean up if its thread is perpetually spinning its PollState instance method?

Revised Code

using System;
using System.Threading;
using System.Windows.Threading;

namespace WPF.Utilities.Common
{
    /// <summary>This class allows a user to easily set up a seperate thread to poll some state,
    /// and set up an event that will fire if the state meets some condition.</summary>
    /// <typeparam name="T">The type of the value returned by the polling delegate.</typeparam>
    public class ConditionMonitor<T> : IDisposable
    {
        #region Private Properties
        private Object multiThreadLock = new Object();
        private Dispatcher originThread = null;
        private Thread monitorThread = null;
        private volatile bool Halted = false;
        #endregion

        #region Delegates
        /// <summary>A delegate provided by the user of this class which returns the current state,
        /// to be tested against a certain condition in the IsConditionMet delegate.</summary>
        public delegate T RequestState();
        public RequestState RequestStateDelegate { get; set; }

        /// <summary>A delegate provided by the user of this class which determines whether given the
        /// current state, the polling program should execute the ConditionMet delegate.</summary>
        public delegate bool IsConditionMet(T state);
        public IsConditionMet IsConditionMetDelegate { get; set; }

        /// <summary>A delegate used to handle ConditionMonitor events.</summary>
        public delegate void ConditionMonitorHandler(ConditionMonitor<T> source, T state);
        /// <summary>An event which fires each time the state is polled (use sparingly).</summary>
        public event ConditionMonitorHandler RequestReceived;
        /// <summary>An event which fires when the condition is met.</summary>
        public event ConditionMonitorHandler ConditionMet;

        /// <summary>A delegate used to handle ConditionMonitor events.</summary>
        public delegate void ConditionMonitorExceptionHandler(ConditionMonitor<T> source, T state, Exception ex);
        /// <summary>An event which fires if an exception is thrown while retrieving the state
        /// or testing whether the condition is met.</summary>
        public event ConditionMonitorExceptionHandler RequestError;
        #endregion

        #region Public Properties
        /// <summary>The time between requests made to the RequestStateDelegate. Default is 1 second (1000ms)</summary>
        public int PollInterval_Milliseconds { get; set; }
        /// <summary>Set to true to automatically halt polling once the condition is met. Default is False.</summary>
        public bool HaltWhenConditionMet { get; set; }
        #endregion

        #region Constructors
        /// <summary>Creates a new instance of a ConditionMonitor</summary>
        public ConditionMonitor()
        {
            originThread = Dispatcher.CurrentDispatcher;
            PollInterval_Milliseconds = (int)TimeSpan.FromSeconds(1).TotalMilliseconds;
            HaltWhenConditionMet = false;
        }
        #endregion

        #region Public Methods
        /// <summary>Begins polling the RequestStateDelegate on a seperate thread.</summary>
        public void BeginMonitoring()
        {
            if( RequestStateDelegate == null ) throw new Exception("No delegate specified for polling - please set the RequestStateDelegate property.");
            lock( multiThreadLock )
            {
                if( monitorThread != null ) throw new Exception("Previous monitoring has not yet been stopped!");
                monitorThread = new Thread(new ThreadStart(PollState));
            }
            Halted = false;
            monitorThread.Start();
        }
        /// <summary>Halts polling and ensures that no more requests will be made or events fired.</summary>
        public void StopMonitoring()
        {
            Halted = true;
        }

        /// <summary>Halts the thread if it is still running so that the instance can be garbage collected.</summary>
        public void Dispose()
        {
            StopMonitoring();
        }
        #endregion

        #region Private Methods
        /// <summary>Responsible for the polling loop and invoking events back on the origin thread.</summary>
        private void PollState()
        {
            while( !Halted )
            {
                T state = default(T);
                bool bConditionMet = false;
                try
                {
                    state = RequestStateDelegate();
                    InvokeEvent(RequestReceived, state);
                    if( IsConditionMetDelegate != null && !Halted )
                    {
                        bConditionMet = IsConditionMetDelegate(state);
                        if( bConditionMet )
                        {
                            InvokeEvent(ConditionMet, state);
                            if( HaltWhenConditionMet ) Halted = true;
                        }
                    }
                }
                catch( Exception ex )
                {
                    InvokeExceptionHandler(state, ex);
                }
                if( !Halted ) Thread.Sleep(PollInterval_Milliseconds);
            }
            monitorThread = null;
        }

        /// <summary>Invokes a delegate of type ConditionMonitorHandler on the origin thread.</summary>
        /// <param name="toInvoke">The delegate to invoke (RequestRecieved or ConditionMet)</param>
        /// <param name="state">The response from the last call to the RequestStateDelegate</param>
        private void InvokeEvent(ConditionMonitorHandler toInvoke, T state)
        {
            if( toInvoke != null && !Halted )
                originThread.BeginInvoke(toInvoke, new object[] { this, state });
        }
        /// <summary>Invokes the exception delegate on the origin thread.</summary>
        /// <param name="state">The response from the last call to the RequestStateDelegate, or null.</param>
        /// <param name="ex">The exception raised while calling the RequestStateDelegate or IsConditionMetDelegate.</param>
        private void InvokeExceptionHandler(T state, Exception ex)
        {
            if( RequestError != null && !Halted )
                originThread.BeginInvoke(RequestError, new object[] { this, state, ex });
        }
        #endregion
    }
}

I am no attempting to implement the polling process using a Timer and the ThreadPool as recommended in the answer. A followup question has been posted here:

Timer to poll a Delegate

\$\endgroup\$

1 Answer 1

9
\$\begingroup\$

I want to do this in a way that consumes minimal resources

Then don't have a separate thread that's not doing anything most of the time. Each thread in .Net consumes 1 MB of memory (both virtual and physical) and some other resources. A better solution would be to run your code on the ThreadPool and don't use Thread.Sleep(). The best way to do this is to use a Timer.

I just want to be sure I haven't set this up in a way where I risk spinning off more than a single thread […]

Actually, there is a chance exactly that will happen if two threads called BeginMonitoring() at the same time. Could that happen in your situation?

[…] or end up with a thread that keeps running even after StopMonitoring has been called.

I think that is also a possibility. The check of Halted might be optimized away, you should mark it as volatile, to make sure that doesn't happen.

Is there anything else I need to do to clean up the thread, or is simply letting PollState return sufficient?

Yes, that is sufficient, just let the method return and the thread will be safely destroyed.

I'm also worried about what happens if someone abandons the ConditionMonitor instance without remembering to call StopMonitoring(). The GC would dispose of the class, but what would happen to the thread is spun up - would it get silently aborted, or throw a huge fuss since it references ConditionMonitor properties?

The GC would do no such thing. The thread you created does have a reference to the instance, so the GC won't “dispose” it. (I use quotes, because disposing in .Net usually means calling IDisposable.Dispose(), that's not what I'm talking about here.) But because of that, the thread would run indefinitely. And a finalizer wouldn't help you either, again because the GC won't touch the instance, because it's still referenced.

A solution here might be to have another object that isn't referenced from PollState() and that would make sure the thread is stopped in its finalizer.


Also, it seems to me you're using virtual too much. It's meant only for methods that you really want to have overridden. And, actually private virtual doesn't make sense and should cause a compile error. Similarly, the default visibility for fields (and non-public methods) should be private, not protected.

\$\endgroup\$
10
  • \$\begingroup\$ Re: private virtual - you're right that was stupid of me, but I do intend to make this class overridable in case one wants to support multiple conditions or additional events. Almost everything's virtual because I can envision the need to change them all in a more complex implementation. Perhaps to that effect, I should just create an interface and have this class implement the interface. \$\endgroup\$
    – Alain
    Jul 5, 2012 at 22:31
  • \$\begingroup\$ Re: Threading - I never imagined this class being instantiated and used from anything other than the main UI thread, so it doesn't attempt to take concurrency into account (aside from any internal concurrency issues that might arise between the instance's own polling thread and functions called from the origin thread.) I'm happy enough if it's safe under the assumption that only the instantiating thread makes any method calls or property sets. \$\endgroup\$
    – Alain
    Jul 5, 2012 at 22:35
  • \$\begingroup\$ Re: Halted being optimized away. I also never thought this possible. Wouldn't the compiler recognize the simple fact that the if( HaltWhenConditionMet ) Halted = true; modifies the value of Halted, so it can't possibly be removed from consideration? \$\endgroup\$
    – Alain
    Jul 5, 2012 at 22:39
  • \$\begingroup\$ Re: Resoures - my bad for not clarifying, a relatively small memory footprint doesn't concern me. I thought CPU wise, it was less computationally expensive to leave one method running (and sleeping the thread) than it was to have a timer spawn up fresh method calls on the stack every few seconds. I use a timer for some UI events that get periodically refreshed, because I can't sleep the UI thread, but since this is a dedicated thread, would Sleep() not be the least (CPU) invasive solution? \$\endgroup\$
    – Alain
    Jul 5, 2012 at 22:43
  • 1
    \$\begingroup\$ Re destructor: The point is that the actual object won't have any references to the dummy object. That way, the dummy object can be GCed. And yeah, if you (as a user of some class) don't dispose of it, you can expect bad things to happen. And while finalizers can be useful as a last resort, they're by their nature quite unreliable. \$\endgroup\$
    – svick
    Jul 5, 2012 at 23:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.