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

标签 c# winforms

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

如果可能的话,这是我喜欢创建的内容。

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 创建的
  • Execute 被调用时,Timebox 创建一个子 AppDomain 来保存一个 TimeboxRuntime 对象引用,并且返回一个代理给它
  • AppDomain 中的 TimeboxRuntime 对象将 Action 作为在子域中执行的输入
  • Timebox 然后创建一个任务来调用 TimeboxRuntime 代理
  • 任务开始( Action 开始执行),“主”线程等待给定的TimeSpan
  • 在给定的 TimeSpan 之后(或任务完成时),无论 Action 是否完成,子 AppDomain 都会被卸载。
  • 如果 action 超时,则抛出 TimeoutException,否则如果 action 抛出异常,则由子 AppDomain 捕获 并返回调用 AppDomain 抛出

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

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

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

关于c# - 如何为各种代码块创建通用超时对象?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34597178/

相关文章:

c# - C# 中的缩放图像

c# - 如何禁用 DateTimePicker 控件上的某些日期?

c# - 使用窗口窗体大小自动调整 directx 控件的大小?

c# - MVC 4.5.2 Response.WriteFile 大于 2gb

c# - 使用透明度时 Windows 应用程序非常慢

c# - 一个方法链能叫LINQ吗?

c# - C# 中的多媒体定时器中断(前两个中断不好)

c# - 将 YAML 反序列化为自定义类型

winforms - 使用 Windows 窗体的 StructureMap

c# - 如何动态创建选项卡