10

I'm running parallel operations on a list of PCs I'm getting from the Active Directory. I was using this method to check PC states, such as whether the computer was online, or if a specific directory existed. However due to the nature of these, sometimes slow, operations, I wanted to include a timeout so my application could continue to operate.

public static T MethodTimeout<T>(Func<T> f, int timeout, out bool completed)
{
    T result = default(T);
    var thread = new Thread(() => result = F());
    thread.Start();
    Completed = thread.Join(Timeout);
    if (!Completed) thread.Abort();
    return result;
}

This works for the most part, but the processing usage seemed to spike a little, and in a few cases I ran into Out of Memory exceptions. So, I since changed the method to use tasks in the hope that the ThreadPool would eliminate the aforementioned problems:

public static T MethodTimeout<T>(Func<T> f, int timeout, out bool completed)
{
    T result = default(T);

    var timedTask = Task.Factory.StartNew(() => result = F());
    Completed = timedTask.Wait(Timeout);

    return result;
}

However, I have a feeling I'm just filling up the ThreadPool with processes that are hanging waiting for these potentially long tasks to finish. Since I'm passing the task's function as a parameter I don't see a way to use a cancellation token. However, my experience is very limited with these classes and I might be missing some superior techniques.

Here's how I've been using the method above:

bool isReachable; // Check if the directory exists (1 second)
MethodTimeout(() => Directory.Exists(startDirectory), 1000, out isReachable);

A quick note, I'm only running the above check after I've already confirmed the PC is online via a WMI call (also using MethodTimeout). I'm aware through my early testing that checking the directory before that ends up being inefficient.

I'm also open to replacing this approach with something better.

Parrish Husband
  • 3,148
  • 18
  • 40
  • BTW, the .Net naming conventions say that parameters of methods should be named in `lowerCase`. – svick Oct 31 '13 at 23:58
  • Also, if you don't understand the problem, blindly changing the way you're calling your code most likely won't help anything. You need to figure out the cause of those problems. – svick Nov 01 '13 at 00:00
  • Does [this](http://stackoverflow.com/q/4238345/945456) help? It takes the approach of wrapping the task in another task. Could you force the user to give you a `Func` that takes a `CancellationTokenSource`? – Jeff B Nov 01 '13 at 00:01
  • Not sure I agree with "blindly", but I'd concede to tunnel vision. I knew trying to manage the two exceptions I was dealing with when using Thread.Start could be mitigated by implementing the threadpool. I probably needed to zoom out and look at the bigger picture, which I ultimately will do. – Parrish Husband Nov 01 '13 at 02:02

3 Answers3

6

I may be the harbinger of bad news here, but this scenario is way more difficult to deal with than most people think. It seems that you are already picking up on this. Using the cooperative cancellation mechanisms is all good, but you have to be able to actually cancel the time consuming operation to be able to use them effectively. The problem is that Directory.Exists can not be cancelled.

The problem with your first solution is that you are aborting a thread. This is not a good idea because it halts the thread at unpredictable points. This may lead to the corruption of data structures for anything that was executing on the call stack when the abort got injected. In this particular case I would not be surprised if the Thread.Abort call actually hung. The reason is that aborts are normally delayed while the execution is in unmanaged code. It is likely Directory.Exists defers to an unmanaged module. If that is the case then the abort will not work anyway.

The problem with the second solution is that you are leaving the task orphaned. That Directory.Exists call would still be executing on a thread pool thread somewhere. This is because you have not actually cancelled it.

To be honest I am not really sure what to do about this. The lack of a cancellable Directory.Exists method is very problematic. My first thought is to attempt a write or read from the directory you want to test as kind of a proxy for checking its existence. The FileStream class does have cancellable operations. In fact, many of the methods even accept a CancellationToken as a parameter. Or you could close the FileStream and that will cancel any pending operations as well. Another option might be to use the Win32 API functions to do the IO. Then you could call CancelSynchronousIo if necessary.

I know this is not much of an answer because all I have really done is tell you what you cannot do, but did not offer a definitive solution. The point is that the best solution starts with a cancellable operation. It is unfortunate that some of the BCL classes do not provide these even when they should.

Brian Gideon
  • 47,849
  • 13
  • 107
  • 150
  • I actually think this is a great answer, thank you for explaining the Directory class. I'll test this direction today and see if I can leverage a method that can be cancelled. – Parrish Husband Nov 01 '13 at 11:58
5

If you are using .Net 4.5 you can do it using CancelAfter:

var cts = new CancellationTokenSource(3000); // Set timeout

Task.Run(() =>
{
    while (!cts.Token.IsCancellationRequested)
    {
        // Doing Work...
    }

}, cts.Token);
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
Damian Drygiel
  • 17,900
  • 4
  • 35
  • 28
0

Could something like this work? You'd have to require that the consumer passes you a Func that takes a CancellationTokenSource. That Func would be responsible to check IsCancellationRequested at the appropriate spot in its code...

public static T MethodTimeout<T>(Func<T, CancellationTokenSource> F, 
                                 int Timeout, out bool Completed)
{
    T result = default(T);
    var source = new CancellationTokenSource(Timeout);
    var timedTask = Task.Factory.StartNew(() => result = F(source));
    Completed = timedTask.Wait(Timeout);
    if(!Completed) source.Cancel();
    return result;
}

My knowledge of TPL is slim to none, so sorry if my code isn't quite right. Haven't got a way to test it with my older version of VS. Corrects/edits welcome :)

Jeff B
  • 8,572
  • 17
  • 61
  • 140
  • I could use this for methods I have direct control over, but wouldn't be able to for something like Directory.Exists. Sounds like I'll need to abandon that route anyway though, so this may prove useful. Thanks. – Parrish Husband Nov 01 '13 at 12:01
  • Wow didn't know `Directory.Exists` could take long enough you might want to cancel it. I'm not sure what you mean by "My loyalty lies with the number of operations per second", but if you're wanting a way to limit the concurrant number of _things_ running, take a look at [RateLimit](http://stackoverflow.com/q/19579935/945456) (non-BCL, but just a small class). – Jeff B Nov 01 '13 at 12:42
  • I was running this yesterday on about 5500 computers, and towards the end I was getting significant hangups on a method I use to enumerate directories on the remote host. I found that adding Directory.Exists significantly sped up the hanging calls. Oddly enough I had already done a WMI check on those computers and determined they were online, but it's possible those calls were due to the PC being shutdown in the time between it was checked and the directories enumerated. However my fix wasn't really a fix, as it just offsets the delay to the threadpool, which is just as bad. – Parrish Husband Nov 01 '13 at 14:14