4
\$\begingroup\$

This is my first attempt at trying to write a "monitor" class to determine if a UNC path is available. I need the monitor to not block the main thread and to also event when the UNC is toggled UP/DOWN so that I can respond to that event.

I'm looking for:

  1. Best practice suggestions
  2. Am I doing this properly? Is there a more efficient way?
  3. Will there be threads left alive? will I eat into the pool until it starves?
  4. Other comments suggestions related to this code or programming context

public class NetworkPathMonitor 
{
    public event EventHandler<PathAvailabilityChangedArgs> PathAvailabilityChanged;
    public event EventHandler Started;
    public event EventHandler Stopping;
    public event EventHandler Stopped;
    public event EventHandler CheckingStatus;

    protected virtual void OnStopping(object sender, EventArgs e)
    {
        EventHandler handler = Stopping;
        if (handler != null)
            handler(sender, e);
    }
    protected virtual void OnCheckingStatus(object sender, EventArgs e)
    {
        EventHandler handler = CheckingStatus;
        if (handler != null)
            handler(sender, e);
    }
    protected virtual void OnStopped(object sender, EventArgs e)
    {
        EventHandler handler = Stopped;
        if (handler != null)
            handler(sender, e);
    }
    protected virtual void OnStarted(object sender, EventArgs e)
    {
        EventHandler handler = Started;
        if (handler != null)
            handler(sender, e);
    }
    protected virtual void OnPathAvailabilityChanged(object sender, PathAvailabilityChangedArgs e)
    {
        EventHandler<PathAvailabilityChangedArgs> handler = PathAvailabilityChanged;
        if (handler != null)
            handler(sender, e);
    }

    private PathState CurrentState { get; set; }

    private CancellationTokenSource TokenSource { get; set; }

    private AutoResetEvent IsStopping { get; set; }

    public string PathToMonitor { get; set; }

    public int Interval { get; set; }

    public NetworkPathMonitor(string pathToMonitor, int interval, PathState expectedState = PathState.Up)
    {
        this.CurrentState = expectedState;
        this.Interval = interval;
        this.PathToMonitor = pathToMonitor;
    }

    public void Stop()
    {
        OnStopping(this, EventArgs.Empty);
        TokenSource.Cancel();
    }

    public void Start()
    {

        IsStopping = new AutoResetEvent(false);
        TokenSource = new CancellationTokenSource();

        var t = Task.Factory.StartNew(() =>
        {
            OnStarted(this, EventArgs.Empty);
            TimeSpan waitInterval = TimeSpan.FromMilliseconds(Interval);
            while (!IsStopping.WaitOne(waitInterval))
            {
                if (this.TokenSource.IsCancellationRequested)
                {
                    //Is this how to properly stop the signal?
                    IsStopping.Set();
                    IsStopping.Dispose();
                    OnStopped(this, EventArgs.Empty);
                    break;
                }

                OnCheckingStatus(this, EventArgs.Empty);
                CheckPathStatus();
            }
        }, TokenSource.Token);

    }


    private void CheckPathStatus()
    {
        //this is synchronous. We just have to wait for it to timeout.
        var currentState = Directory.Exists(PathToMonitor) ? PathState.Up : PathState.Down;

        var shouldEvent = currentState != CurrentState;
        CurrentState = currentState;

        if (shouldEvent)
        {
            OnPathAvailabilityChanged(this, new PathAvailabilityChangedArgs() { Path = PathToMonitor, PathState = CurrentState });
        }

    }
}


public enum PathState
{
    Up,
    Down
}
public class PathAvailabilityChangedArgs : EventArgs
{
    public string Path { get; set; }
    public PathState PathState { get; set; }
    public PathAvailabilityChangedArgs() { }
    public PathAvailabilityChangedArgs(string path)
    {
        Path = path;
    }
}
\$\endgroup\$
0

2 Answers 2

3
\$\begingroup\$

The implementation you have will forever consume one thread from the ThreadPool. That's not the end of the world, but not ideal either. An easy solution would be to use an overload of Task.Factory.StartNew that accepts a TaskCreationOptions argument, and specify TaskCreationOptions.LongRunning. That will let the system handle it appropriately. Another option would be starting a new thread yourself.

In the task delegate (the code inside StartNew()) I would put a global try/catch block. Any exceptions that might be thrown in there will cause your process to be killed. At the very least you'd want to log those somewhere just so you have some idea what's going on.

I think you're going to Dispose() the IsStopping event on the first time through the loop, then try to use it on the second time through, which will probably cause an exception. I would just remove the AutoResetEvent entirely and use CancellationTokenSource instead. You can use TokenSource.Token.WaitHandle.WaitOne() to wait on instead of the AutoResetEvent. This also has the benefit that when Stop() is called, any pending wait will end immediately.

I would make the Interval property a TimeSpan instead of an int. It's just more clear to users what it means - they don't have to wonder whether it's seconds, milliseconds, or what.

\$\endgroup\$
5
  • \$\begingroup\$ Very good feeback. It's actually not as bad as I thought it would be. All of your suggestions are valid. I will attempt to make these changes and post a revision when complete. Additionally, if there is no more feedback afterwards, I will mark this as the answer. \$\endgroup\$
    – Hardrada
    Commented Dec 22, 2012 at 16:47
  • \$\begingroup\$ Just a note about the disposing of the AutoEvent: That only happened upon cancellation (token.Cancel()). When the call to cancel was made, the loop would only then check the cancellation status and dispose. The code as entered in my OP didn't throw any errors during testing, however, that's not to say that it is absolute. I just can't say for sure due to lack of confidence. \$\endgroup\$
    – Hardrada
    Commented Dec 22, 2012 at 17:37
  • \$\begingroup\$ I have updated the code per the suggestions. However, I am uncertain how to handle exceptions from within the Task thread. I read albahari.com/threading/… and thought I had applied it properly, but it doesn't appear to bubble up. \$\endgroup\$
    – Hardrada
    Commented Dec 22, 2012 at 20:27
  • \$\begingroup\$ Since this is a simple case I would probably throw a try/catch block inside the thread method, rather than using continuations. But the way you've done it looks OK to me. Have you tried intentionally causing an exception and following it in the debugger? \$\endgroup\$
    – breischl
    Commented Dec 23, 2012 at 16:46
  • \$\begingroup\$ I have done an intentional throw and it never popped in the debugger. Everything looks and runs as expected. Thank you for your time, effort and expertise. I've marked your reply as the answer. \$\endgroup\$
    – Hardrada
    Commented Dec 23, 2012 at 18:22
2
\$\begingroup\$

Just to add to breishl's answer:

  • don't use private properties, just use fields instead (private CancellationTokenSource TokenSource { get; set; } => private CancellationTokenSource _tokenSource;)
  • AutoResetEvent is not needed as already noted, but even now it's used incorrectly, you should have used ManualResetEvent instead (again, not needed here)
  • you can also wait (await) with cancellationToken using Task.Delay(TimeSpan, CancellationToken) method.
\$\endgroup\$
2
  • \$\begingroup\$ I updated my comment to include a formerly missing requirement of .NET 4.0. I have taken note of the incorrect usage of AutoResetEvent and will research more for my own knowledge; however, it seems it's somewhat obsolete now with the existence of Tasks? Can you confirm that? \$\endgroup\$
    – Hardrada
    Commented Dec 22, 2012 at 19:53
  • 1
    \$\begingroup\$ I wouldn't say that ManualResetEvent and AutoResetEvent are already obsolete. That's true that most of day-to-day work is covered with new async API (Tasks in .NET 4.0 and async compiler support in .NET 4.5), but there are cases when you need to synchronise work done in different threads. It's like the main use case of Monitor class is covered with lock keyword, but there may be cases when you need features like Monitor.Wait and Monitor.Pulse \$\endgroup\$
    – almaz
    Commented Dec 22, 2012 at 21:49

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.