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

我有一系列代码块耗时太长。 失败时我不需要任何技巧。 事实上,当这些块花费太长时间时,我想抛出一个异常,并通过我们的标准错误处理而退出。 我宁愿不从每个块中创建方法(这是我目前看到的唯一建议),因为它需要对代码库进行重大改写。

这是我如果可能的创建。

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.
    }

 ...
}

我创建了一个简单的对象来使用Timer类来完成此操作。 (对于喜欢复制/粘贴的用户请注意:此代码不起作用!!)

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();
            }
        }
    }

它不起作用,因为System.Timers.Timer类捕获,吸收和忽略任何抛出的异常,正如我发现的那样,它会打败我的设计。 没有完全重新设计的创建这个类/功能的任何其他方式?

这在两小时前似乎很简单,但是让我非常头疼。


好吧,我花了一些时间在这一个上,我想我有一个解决方案可以为你工作,而不必改变你的代码。

以下是您将如何使用我创建的Timebox类。

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

}

以下是Timebox工作原理。

  • Timebox对象是使用给定的Timespan创建的
  • 当调用ExecuteTimebox创建一个子AppDomain来存放TimeboxRuntime对象引用,并向它返回一个代理
  • AppDomainTimeboxRuntime对象将一个Action作为输入在子域中执行
  • Timebox然后创建一个任务来调用TimeboxRuntime代理
  • 任务开始(并且动作执行开始),并且“主”线程等待给定的TimeSpan
  • 在给定的TimeSpan (或任务完成时),无论Action是否完成,子AppDomain被卸载。
  • 如果action超时,则引发TimeoutException ;否则,如果action引发异常,则由子AppDomain捕获并由调用的AppDomain返回
  • 缺点是您的程序需要提升足够的权限才能创建AppDomain

    下面是一个示例程序,演示了它的工作原理(如果包含正确的using ,我相信您可以复制粘贴该内容)。 如果您有兴趣,我也创建了这个要点。

    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;
            }
        }
    }
    

    编辑:

    我之所以选择AppDomain而不是ThreadTask ,是因为对于任意代码[1] [2] [3],没有用于终止ThreadTask的防弹方法。 根据您的需求,AppDomain似乎是对我最好的方法。


    以下是超时的异步实现:

       ...
          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();
          }
        }
    

    这并不是说我提供建议,但你可以通过tokenSource而不是它的TokenWorkMethod并定期做tokenSource.CancelAfter(200)添加,如果你一定有更多的时间你是不是在一个点,可以是死锁定(等待一个HTTP调用),但我认为这将是一个多线程的深奥方法。

    相反,如果你需要处理IO多线程(比如文件压缩,下载等)并避免死锁,你的线程应该尽可能快(最小的IO),一个线程可以序列化资源(生产者),而其他线程处理队列完全可能。


    我非常喜欢使用陈述的视觉想法。 但是 ,这不是一个可行的解决方案。 为什么? 那么,子线程(using语句中的object / thread / timer)不能破坏主线程并注入异常,从而导致它停止正在做的事情并跳到最近的try / catch。 这就是这一切的结果。 我越坐在这里工作,就越发现。

    总之,这不能按照我想要的方式完成。

    但是,我采取了Pieter的方法,并且破坏了我的代码。 它确实引入了一些可读性问题,但我试图用评论等来缓解它们。

    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");
        }
    
     ...
    }
    

    由于我在每个主要部分的三到四个位置执行此操作,因此读起来更加困难。 但是,它工作得很好。

    链接地址: http://www.djcxy.com/p/89525.html

    上一篇: How to create a generic timeout object for various code blocks?

    下一篇: Selected state of text in a segmented control in Xcode