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

时间:2016-01-04 18:09:45

标签: c# winforms

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

如果可能,我会 LIKE 创建。

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类捕获,吸收和忽略其中抛出的任何异常,正如我所发现的那样 - 击败了我的设计。在没有进行全面重新设计的情况下创建此类/功能的任何其他方式?

两个小时前这看起来很简单,但让我很头疼。

3 个答案:

答案 0 :(得分:2)

这是超时的异步实现:

   ...
      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而不是Token传递给WorkMethod并定期tokenSource.CancelAfter(200)添加更多的时间,如果你确定你不在一个可以死锁的地方(等待HTTP呼叫),但我认为这将是一种深奥的多线程方法。

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

答案 1 :(得分:2)

好的,我已经花了一些时间在这个上面,我想我有一个适合你的解决方案,而不必更改你的代码。

以下是如何使用我创建的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对象
  • 调用Execute时,Timebox会创建一个子AppDomain来保存TimeboxRuntime对象引用,并向其返回一个代理
  • TimeboxRuntime中的AppDomain对象需要Action作为输入才能在子域中执行
  • Timebox然后创建一个任务来调用TimeboxRuntime代理
  • 任务开始(动作执行开始)," main"线程等待给定的TimeSpan
  • 在给定TimeSpan之后(或任务完成时),无论AppDomain是否已完成,都会卸载子Action
  • 如果TimeoutException超时,则抛出action,否则如果action抛出异常,它将被子AppDomain捕获并返回调用{{1}投掷

缺点是您的程序需要足够高的权限来创建AppDomain

这是一个演示如何工作的示例程序(我相信如果包含正确的AppDomain,您可以复制粘贴它)。如果您有兴趣,我还创建了this gist

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; } } } 代替AppDomainThread s的原因是因为没有用于终止Task或{{}的防弹方法1}} s代表任意代码[1] [2] [3]。根据您的要求,AppDomain对我来说似乎是最好的方法。

答案 2 :(得分:0)

我真的很喜欢使用声明的视觉概念。 然而,这不是一个可行的解决方案。为什么?好吧,子线程(using语句中的对象/线程/计时器)不能破坏主线程并注入异常,从而导致它停止正在进行的操作并跳转到最近的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");
    }

 ...
}

由于我在每个主要部分的三到四个地方这样做,这确实更难以阅读。但是,它运作得很好。