Reputation: 263087
I'm currently writing a web services based front-end to an existing application. To do that, I'm using the WCF LOB Adapter SDK, which allows one to create custom WCF bindings that expose external data and operations as web services.
The SDK provides a few interfaces to implement, and some of their methods are time-constrained: the implementation is expected to complete its work within a specified timespan or throw a TimeoutException.
Investigations led me to the question "Implement C# Generic Timeout", which wisely advises to use a worker thread. Armed with that knowledge, I can write:
public MetadataRetrievalNode[] Browse(string nodeId, int childStartIndex,
int maxChildNodes, TimeSpan timeout)
{
Func<MetadataRetrievalNode[]> work = () => {
// Return computed metadata...
};
IAsyncResult result = work.BeginInvoke(null, null);
if (result.AsyncWaitHandle.WaitOne(timeout)) {
return work.EndInvoke(result);
} else {
throw new TimeoutException();
}
}
However, the consensus is not clear about what to do with the worker thread if it times out. One can just forget about it, like the code above does, or one can abort it:
public MetadataRetrievalNode[] Browse(string nodeId, int childStartIndex,
int maxChildNodes, TimeSpan timeout)
{
Thread workerThread = null;
Func<MetadataRetrievalNode[]> work = () => {
workerThread = Thread.CurrentThread;
// Return computed metadata...
};
IAsyncResult result = work.BeginInvoke(null, null);
if (result.AsyncWaitHandle.WaitOne(timeout)) {
return work.EndInvoke(result);
} else {
workerThread.Abort();
throw new TimeoutException();
}
}
Now, aborting a thread is widely considered as wrong. It breaks work in progress, leaks resources, messes with locking and does not even guarantee the thread will actually stop running. That said, HttpResponse.Redirect()
aborts a thread every time it's called, and IIS seems to be perfectly happy with that. Maybe it's prepared to deal with it somehow. My external application probably isn't.
On the other hand, if I let the worker thread run its course, apart from the resource contention increase (less available threads in the pool), wouldn't memory be leaked anyway, because work.EndInvoke()
never gets called? More specifically, wouldn't the MetadataRetrievalNode[]
array returned by work
remain around forever?
Is this only a matter of choosing the lesser of two evils, or is there a way not to abort the worker thread and still reclaim the memory used by BeginInvoke()
?
Upvotes: 5
Views: 1244
Reputation: 71591
The answer depends on the type of work your worker thread is performing. My guess is it's working with external resources like a data connection. Thread.Abort() is indeed evil in any case of threads working with hooks to unmanaged resources, no matter how well-wrapped.
Basically, you want your service to give up if it times out. At this point, theoretically, the caller no longer cares how long the thread's going to take; it only cares that it's "too long", and should move on. Barring a bug in the worker thread's running method, it WILL end eventually; the caller just no longer cares when because it's not waiting any longer.
Now, if the reason the thread timed out is because it's caught in an infinite loop, or is told to wait forever on some other operation like a service call, then you have a problem that you should fix, but the fix is not to kill the thread. That would be analagous to sending your kid into a grocery store to buy bread while you wait in the car. If your kid keeps spending 15 minutes in the store when you think it should take 5, you eventually get curious, go in and find out what they're doing. If it's not what you thought they should be doing, like they've spent all the time looking at pots & pans, you "correct" their behavior for future occasions. If you go in and see your kid standing in a long checkout line, then you just start waiting longer. In neither of these cases should you press the button that detonates the explosive vest they're wearing; that just makes a big mess that will likely interfere with the next kid's ability to do the same errand later.
Upvotes: 1
Reputation: 48959
Well, first off Thread.Abort
is not nearly as bad as it used it to be. There were several improvements made to the CLR in 2.0 that fixed several of the major issues with aborting threads. It is still bad, mind you, so avoiding it is the best course of action. If you must resort to aborting threads then at the very least you should consider tearing down the application domain from where the abort originated. That is going to be incredibly invasive in most scenarios and would not resolve the possible corruption of unmanaged resources.
Aside from that, aborting in this case is going to have other implications. The most important being that you are attempting to abort a ThreadPool
thread. I am really not sure what the end result of that would be and it could be different depending on which version of the framework is in play.
The best course of action is to have your Func<MetadataRetrievalNode[]>
delegate poll a variable at safe points to see if it should terminate execution on its own.
public MetadataRetrievalNode[] Browse(string nodeId, int childStartIndex, int maxChildNodes, TimeSpan timeout)
{
bool terminate = false;
Func<MetadataRetrievalNode[]> work =
() =>
{
// Do some work.
Thread.MemoryBarrier(); // Ensure a fresh read of the terminate variable.
if (terminate) throw new InvalidOperationException();
// Do some work.
Thread.MemoryBarrier(); // Ensure a fresh read of the terminate variable.
if (terminate) throw new InvalidOperationException();
// Return computed metadata...
};
IAsyncResult result = work.BeginInvoke(null, null);
terminate = !result.AsyncWaitHandle.WaitOne(timeout);
return work.EndInvoke(result); // This blocks until the delegate completes.
}
The tricky part is how to deal with blocking calls inside your delegate. Obviously, you cannot check the terminate
flag if the delegate is in the middle of a blocking call. But, assuming the blocking call is initiated from one of the canned BCL waiting mechansisms (WaitHandle.WaitOne
, Monitor.Wait
, etc.) then you could use Thread.Interrupt
to "poke" it and that should immediately unblock it.
Upvotes: 6