How to create a generic timeout object for various code blocks?
I have a series of code blocks that are taking too long. I don't need any finesse when it fails. In fact, I want to throw an exception when these blocks take too long, and just fall out through our standard error handling. I would prefer to NOT create methods out of each block (which are the only suggestions I've seen so far), as it would require a major rewrite of the code base.
Here's what I would LIKE to create, if possible.
public void MyMethod( ... )
{
...
using (MyTimeoutObject mto = new MyTimeoutObject(new TimeSpan(0,0,30)))
{
// Everything in here must complete within the timespan
// or mto will throw an exception. When the using block
// disposes of mto, then the timer is disabled and
// disaster is averted.
}
...
}
I've created a simple object to do this using the Timer class. (NOTE for those that like to copy/paste: THIS CODE DOES NOT WORK!!)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
public class MyTimeoutObject : IDisposable
{
private Timer timer = null;
public MyTimeoutObject (TimeSpan ts)
{
timer = new Timer();
timer.Elapsed += timer_Elapsed;
timer.Interval = ts.TotalMilliseconds;
timer.Start();
}
void timer_Elapsed(object sender, ElapsedEventArgs e)
{
throw new TimeoutException("A code block has timed out.");
}
public void Dispose()
{
if (timer != null)
{
timer.Stop();
}
}
}
It does not work because the System.Timers.Timer class captures, absorbs and ignores any exceptions thrown within, which -- as I've discovered -- defeats my design. Any other way of creating this class/functionality without a total redesign?
This seemed so simple two hours ago, but is causing me much headache.
OK, I've spent some time on this one and I think I have a solution that will work for you without having to change your code all that much.
The following is how you would use the Timebox
class that I created.
public void MyMethod( ... ) {
// some stuff
// instead of this
// using(...){ /* your code here */ }
// you can use this
var timebox = new Timebox(TimeSpan.FromSeconds(1));
timebox.Execute(() =>
{
/* your code here */
});
// some more stuff
}
Here's how Timebox
works.
Timebox
object is created with a given Timespan
Execute
is called, the Timebox
creates a child AppDomain
to hold a TimeboxRuntime
object reference, and returns a proxy to it TimeboxRuntime
object in the child AppDomain
takes an Action
as input to execute within the child domain Timebox
then creates a task to call the TimeboxRuntime
proxy TimeSpan
TimeSpan
(or when the task completes), the child AppDomain
is unloaded whether the Action
was completed or not. TimeoutException
is thrown if action
times out, otherwise if action
throws an exception, it is caught by the child AppDomain
and returned for the calling AppDomain
to throw A downside is that your program will need elevated enough permissions to create an AppDomain
.
Here is a sample program which demonstrates how it works (I believe you can copy-paste this, if you include the correct using
s). I also created this gist if you are interested.
public class Program
{
public static void Main()
{
try
{
var timebox = new Timebox(TimeSpan.FromSeconds(1));
timebox.Execute(() =>
{
// do your thing
for (var i = 0; i < 1000; i++)
{
Console.WriteLine(i);
}
});
Console.WriteLine("Didn't Time Out");
}
catch (TimeoutException e)
{
Console.WriteLine("Timed Out");
// handle it
}
catch(Exception e)
{
Console.WriteLine("Another exception was thrown in your timeboxed function");
// handle it
}
Console.WriteLine("Program Finished");
Console.ReadLine();
}
}
public class Timebox
{
private readonly TimeSpan _ts;
public Timebox(TimeSpan ts)
{
_ts = ts;
}
public void Execute(Action func)
{
AppDomain childDomain = null;
try
{
// Construct and initialize settings for a second AppDomain. Perhaps some of
// this is unnecessary but perhaps not.
var domainSetup = new AppDomainSetup()
{
ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile,
ApplicationName = AppDomain.CurrentDomain.SetupInformation.ApplicationName,
LoaderOptimization = LoaderOptimization.MultiDomainHost
};
// Create the child AppDomain
childDomain = AppDomain.CreateDomain("Timebox Domain", null, domainSetup);
// Create an instance of the timebox runtime child AppDomain
var timeboxRuntime = (ITimeboxRuntime)childDomain.CreateInstanceAndUnwrap(
typeof(TimeboxRuntime).Assembly.FullName, typeof(TimeboxRuntime).FullName);
// Start the runtime, by passing it the function we're timboxing
Exception ex = null;
var timeoutOccurred = true;
var task = new Task(() =>
{
ex = timeboxRuntime.Run(func);
timeoutOccurred = false;
});
// start task, and wait for the alloted timespan. If the method doesn't finish
// by then, then we kill the childDomain and throw a TimeoutException
task.Start();
task.Wait(_ts);
// if the timeout occurred then we throw the exception for the caller to handle.
if(timeoutOccurred)
{
throw new TimeoutException("The child domain timed out");
}
// If no timeout occurred, then throw whatever exception was thrown
// by our child AppDomain, so that calling code "sees" the exception
// thrown by the code that it passes in.
if(ex != null)
{
throw ex;
}
}
finally
{
// kill the child domain whether or not the function has completed
if(childDomain != null) AppDomain.Unload(childDomain);
}
}
// don't strictly need this, but I prefer having an interface point to the proxy
private interface ITimeboxRuntime
{
Exception Run(Action action);
}
// Need to derive from MarshalByRefObject... proxy is returned across AppDomain boundary.
private class TimeboxRuntime : MarshalByRefObject, ITimeboxRuntime
{
public Exception Run(Action action)
{
try
{
// Nike: just do it!
action();
}
catch(Exception e)
{
// return the exception to be thrown in the calling AppDomain
return e;
}
return null;
}
}
}
EDIT:
The reason I went with an AppDomain
instead of Thread
s or Task
s only, is because there is no bullet proof way for terminating Thread
s or Task
s for arbitrary code [1][2][3]. An AppDomain, for your requirements, seemed like the best approach to me.
Here's an async implementation of timeouts:
...
private readonly semaphore = new SemaphoreSlim(1,1);
...
// total time allowed here is 100ms
var tokenSource = new CancellationTokenSource(100);
try{
await WorkMethod(parameters, tokenSource.Token); // work
} catch (OperationCancelledException ocx){
// gracefully handle cancellations:
label.Text = "Operation timed out";
}
...
public async Task WorkMethod(object prm, CancellationToken ct){
try{
await sem.WaitAsync(ct); // equivalent to lock(object){...}
// synchronized work,
// call tokenSource.Token.ThrowIfCancellationRequested() or
// check tokenSource.IsCancellationRequested in long-running blocks
// and pass ct to other tasks, such as async HTTP or stream operations
} finally {
sem.Release();
}
}
NOT that I advise it, but you could pass the tokenSource
instead of its Token
into WorkMethod
and periodically do tokenSource.CancelAfter(200)
to add more time if you're certain you're not at a spot that can be dead-locked (waiting on an HTTP call) but I think that would be an esoteric approach to multithreading.
Instead your threads should be as fast as possible (minimum IO) and one thread can serialize the resources (producer) while others process a queue (consumers) if you need to deal with IO multithreading (say file compression, downloads etc) and avoid deadlock possibility altogether.
I really liked the visual idea of a using statement. However , that is not a viable solution. Why? Well, a sub-thread (the object/thread/timer within the using statement) cannot disrupt the main thread and inject an exception, thus causing it to stop what it was doing and jump to the nearest try/catch. That's what it all boils down to. The more I sat and worked with this, the more that came to light.
In short, it can't be done the way I wanted to do it.
However, I've taken Pieter's approach and mangled my code a bit. It does introduce some readability issues, but I've tried to mitigate them with comments and such.
public void MyMethod( ... )
{
...
// Placeholder for thread to kill if the action times out.
Thread threadToKill = null;
Action wrappedAction = () =>
{
// Take note of the action's thread. We may need to kill it later.
threadToKill = Thread.CurrentThread;
...
/* DO STUFF HERE */
...
};
// Now, execute the action. We'll deal with the action timeouts below.
IAsyncResult result = wrappedAction.BeginInvoke(null, null);
// Set the timeout to 10 minutes.
if (result.AsyncWaitHandle.WaitOne(10 * 60 * 1000))
{
// Everything was successful. Just clean up the invoke and get out.
wrappedAction.EndInvoke(result);
}
else
{
// We have timed out. We need to abort the thread!!
// Don't let it continue to try to do work. Something may be stuck.
threadToKill.Abort();
throw new TimeoutException("This code block timed out");
}
...
}
Since I'm doing this in three or four places per major section, this does get harder to read over. However, it works quite well.
链接地址: http://www.djcxy.com/p/89526.html上一篇: 如何修改与Python中特定正则表达式匹配的文本?
下一篇: 如何为各种代码块创建通用超时对象?