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.

  • A Timebox object is created with a given Timespan
  • When Execute is called, the Timebox creates a child AppDomain to hold a TimeboxRuntime object reference, and returns a proxy to it
  • The 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
  • The task is started (and the action execution starts), and the "main" thread waits for for as long as the given TimeSpan
  • After the given TimeSpan (or when the task completes), the child AppDomain is unloaded whether the Action was completed or not.
  • A 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中特定正则表达式匹配的文本?

    下一篇: 如何为各种代码块创建通用超时对象?