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

本文关键字:创建 超时 对象 代码 | 更新日期: 2023-09-27 18:28:52

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

如果可能的话,这就是我想创建的内容。

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的工作原理。

  • 使用给定Timespan创建Timebox对象
  • 调用 Execute 时,Timebox会创建一个子AppDomain来保存TimeboxRuntime对象引用,并向其返回代理
  • AppDomain中的TimeboxRuntime对象将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而不是仅使用 Thread s 或 Task s,是因为没有防弹方法来终止任意代码的 Thread s 或 Task s [1][2][3]。满足你要求的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而不是其Token传递到WorkMethod中,如果您确定您不在可能死锁的位置(等待HTTP调用(,则可以定期tokenSource.CancelAfter(200)增加更多时间,但我认为这将是一种深奥的多线程方法。

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

我真的很喜欢 using 语句的视觉想法。然而,这不是一个可行的解决办法。为什么?好吧,子线程(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");
    }
 ...
}

由于我在每个主要部分的三四个地方都这样做,因此确实很难阅读。但是,它运行良好。