ChatGPT解决这个技术问题 Extra ChatGPT

实现 C# 通用超时

我正在寻找实现通用方法的好主意,以使单行(或匿名委托)代码在超时时执行。

TemperamentalClass tc = new TemperamentalClass();
tc.DoSomething();  // normally runs in 30 sec.  Want to error at 1 min

我正在寻找一种可以在我的代码与气质代码交互的许多地方优雅地实现的解决方案(我无法更改)。

此外,如果可能的话,我希望停止执行有问题的“超时”代码。

只是提醒任何查看以下答案的人:他们中的许多人使用 Thread.Abort 这可能非常糟糕。在您的代码中实现 Abort 之前,请阅读有关此的各种评论。有时它可能是合适的,但这种情况很少见。如果您不完全了解 Abort 做什么或不需要它,请实施以下不使用它的解决方案之一。它们是没有那么多选票的解决方案,因为它们不符合我的问题的需要。
有关 thread.Abort 危险的详细信息,请阅读 Eric Lippert 的这篇文章:blogs.msdn.com/b/ericlippert/archive/2010/02/22/…

M
Milad

这里真正棘手的部分是通过将执行程序线程从 Action 传递回可以中止的地方来终止长时间运行的任务。我通过使用一个包装的委托来实现这一点,该委托将要杀死的线程传递到创建 lambda 的方法中的一个局部变量中。

我提交这个例子,为了你的享受。您真正感兴趣的方法是 CallWithTimeout。这将通过中止它并吞下 ThreadAbortException 来取消长时间运行的线程:

用法:

class Program
{

    static void Main(string[] args)
    {
        //try the five second method with a 6 second timeout
        CallWithTimeout(FiveSecondMethod, 6000);

        //try the five second method with a 4 second timeout
        //this will throw a timeout exception
        CallWithTimeout(FiveSecondMethod, 4000);
    }

    static void FiveSecondMethod()
    {
        Thread.Sleep(5000);
    }

做这项工作的静态方法:

    static void CallWithTimeout(Action action, int timeoutMilliseconds)
    {
        Thread threadToKill = null;
        Action wrappedAction = () =>
        {
            threadToKill = Thread.CurrentThread;
            try
            {
                action();
            }
            catch(ThreadAbortException ex){
               Thread.ResetAbort();// cancel hard aborting, lets to finish it nicely.
            }
        };

        IAsyncResult result = wrappedAction.BeginInvoke(null, null);
        if (result.AsyncWaitHandle.WaitOne(timeoutMilliseconds))
        {
            wrappedAction.EndInvoke(result);
        }
        else
        {
            threadToKill.Abort();
            throw new TimeoutException();
        }
    }

}

为什么要捕获(ThreadAbortException)? AFAIK 你不能真正捕捉到 ThreadAbortException (它会在 catch 块离开后重新抛出)。
Thread.Abort() 使用起来非常危险,它不应该与常规代码一起使用,只有保证安全的代码才应该被中止,例如 Cer.Safe 的代码,使用受约束的执行区域和安全句柄。任何代码都不应该这样做。
虽然 Thread.Abort() 很糟糕,但它远没有进程失控并使用 PC 拥有的每个 CPU 周期和内存字节那么糟糕。但是您向可能认为此代码有用的任何其他人指出潜在问题是正确的。
我不敢相信这是被接受的答案,一定有人没有在这里阅读评论,或者答案在评论之前被接受并且那个人没有检查他的回复页面。 Thread.Abort 不是解决方案,它只是您需要解决的另一个问题!
你是不看评论的人。正如上面chilltemp所说,他正在调用他无法控制的代码 - 并希望它中止。如果他希望它在他的进程中运行,他除了 Thread.Abort() 之外别无选择。你说得对,Thread.Abort 很糟糕——但就像chilltemp 所说,其他事情更糟!
P
Patrick from NDepend team

我们在生产中大量使用这样的代码:

var result = WaitFor<Result>.Run(1.Minutes(), () => service.GetSomeFragileResult());

实现是开源的,即使在并行计算场景中也能高效工作,并且作为 Lokad Shared Libraries 的一部分提供

/// <summary>
/// Helper class for invoking tasks with timeout. Overhead is 0,005 ms.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
[Immutable]
public sealed class WaitFor<TResult>
{
    readonly TimeSpan _timeout;

    /// <summary>
    /// Initializes a new instance of the <see cref="WaitFor{T}"/> class, 
    /// using the specified timeout for all operations.
    /// </summary>
    /// <param name="timeout">The timeout.</param>
    public WaitFor(TimeSpan timeout)
    {
        _timeout = timeout;
    }

    /// <summary>
    /// Executes the spcified function within the current thread, aborting it
    /// if it does not complete within the specified timeout interval. 
    /// </summary>
    /// <param name="function">The function.</param>
    /// <returns>result of the function</returns>
    /// <remarks>
    /// The performance trick is that we do not interrupt the current
    /// running thread. Instead, we just create a watcher that will sleep
    /// until the originating thread terminates or until the timeout is
    /// elapsed.
    /// </remarks>
    /// <exception cref="ArgumentNullException">if function is null</exception>
    /// <exception cref="TimeoutException">if the function does not finish in time </exception>
    public TResult Run(Func<TResult> function)
    {
        if (function == null) throw new ArgumentNullException("function");

        var sync = new object();
        var isCompleted = false;

        WaitCallback watcher = obj =>
            {
                var watchedThread = obj as Thread;

                lock (sync)
                {
                    if (!isCompleted)
                    {
                        Monitor.Wait(sync, _timeout);
                    }
                }
                   // CAUTION: the call to Abort() can be blocking in rare situations
                    // http://msdn.microsoft.com/en-us/library/ty8d3wta.aspx
                    // Hence, it should not be called with the 'lock' as it could deadlock
                    // with the 'finally' block below.

                    if (!isCompleted)
                    {
                        watchedThread.Abort();
                    }
        };

        try
        {
            ThreadPool.QueueUserWorkItem(watcher, Thread.CurrentThread);
            return function();
        }
        catch (ThreadAbortException)
        {
            // This is our own exception.
            Thread.ResetAbort();

            throw new TimeoutException(string.Format("The operation has timed out after {0}.", _timeout));
        }
        finally
        {
            lock (sync)
            {
                isCompleted = true;
                Monitor.Pulse(sync);
            }
        }
    }

    /// <summary>
    /// Executes the spcified function within the current thread, aborting it
    /// if it does not complete within the specified timeout interval.
    /// </summary>
    /// <param name="timeout">The timeout.</param>
    /// <param name="function">The function.</param>
    /// <returns>result of the function</returns>
    /// <remarks>
    /// The performance trick is that we do not interrupt the current
    /// running thread. Instead, we just create a watcher that will sleep
    /// until the originating thread terminates or until the timeout is
    /// elapsed.
    /// </remarks>
    /// <exception cref="ArgumentNullException">if function is null</exception>
    /// <exception cref="TimeoutException">if the function does not finish in time </exception>
    public static TResult Run(TimeSpan timeout, Func<TResult> function)
    {
        return new WaitFor<TResult>(timeout).Run(function);
    }
}

这段代码还是有问题的,你可以试试这个小测试程序:

      static void Main(string[] args) {

         // Use a sb instead of Console.WriteLine() that is modifying how synchronous object are working
         var sb = new StringBuilder();

         for (var j = 1; j < 10; j++) // do the experiment 10 times to have chances to see the ThreadAbortException
         for (var ii = 8; ii < 15; ii++) {
            int i = ii;
            try {

               Debug.WriteLine(i);
               try {
                  WaitFor<int>.Run(TimeSpan.FromMilliseconds(10), () => {
                     Thread.Sleep(i);
                     sb.Append("Processed " + i + "\r\n");
                     return i;
                  });
               }
               catch (TimeoutException) {
                  sb.Append("Time out for " + i + "\r\n");
               }

               Thread.Sleep(10);  // Here to wait until we get the abort procedure
            }
            catch (ThreadAbortException) {
               Thread.ResetAbort();
               sb.Append(" *** ThreadAbortException on " + i + " *** \r\n");
            }
         }

         Console.WriteLine(sb.ToString());
      }
   }

有一个竞争条件。很明显,在调用方法 WaitFor<int>.Run() 之后会引发 ThreadAbortException。我没有找到解决此问题的可靠方法,但是通过相同的测试,我无法重现 TheSoftwareJedi 接受的答案的任何问题。

https://i.stack.imgur.com/iX0X8.png


这是我实现的,它可以处理我喜欢和需要的参数和返回值。谢谢里纳特
只是我们用来标记不可变类的一个属性(不变性由 Mono Cecil 在单元测试中验证)
这是一个等待发生的死锁(我很惊讶你还没有观察到它)。您对watchedThread.Abort() 的调用在锁内,也需要在finally 块中获取。这意味着当 finally 块等待锁时(因为 watchThread 在 Wait() 返回和 Thread.Abort() 之间有它),watchedThread.Abort() 调用也将无限期地阻塞等待 finally 完成(它永不)。 Therad.Abort() 可以在受保护的代码区域正在运行时阻塞 - 导致死锁,请参阅 - msdn.microsoft.com/en-us/library/ty8d3wta.aspx
特技开发,非常感谢。出于某种原因,死锁的发生似乎很少见,但我们仍然修复了代码:-)
如何调用 void 类型的函数?
M
Marc Gravell

好吧,你可以用委托做事(BeginInvoke,用回调设置一个标志——原始代码等待那个标志或超时)——但问题是很难关闭正在运行的代码。例如,杀死(或暂停)一个线程是危险的......所以我认为没有一种简单的方法可以稳健地做到这一点。

我会发布这个,但请注意它并不理想 - 它不会停止长时间运行的任务,并且在失败时不会正确清理。

    static void Main()
    {
        DoWork(OK, 5000);
        DoWork(Nasty, 5000);
    }
    static void OK()
    {
        Thread.Sleep(1000);
    }
    static void Nasty()
    {
        Thread.Sleep(10000);
    }
    static void DoWork(Action action, int timeout)
    {
        ManualResetEvent evt = new ManualResetEvent(false);
        AsyncCallback cb = delegate {evt.Set();};
        IAsyncResult result = action.BeginInvoke(cb, null);
        if (evt.WaitOne(timeout))
        {
            action.EndInvoke(result);
        }
        else
        {
            throw new TimeoutException();
        }
    }
    static T DoWork<T>(Func<T> func, int timeout)
    {
        ManualResetEvent evt = new ManualResetEvent(false);
        AsyncCallback cb = delegate { evt.Set(); };
        IAsyncResult result = func.BeginInvoke(cb, null);
        if (evt.WaitOne(timeout))
        {
            return func.EndInvoke(result);
        }
        else
        {
            throw new TimeoutException();
        }
    }

我很高兴能杀死我身上染红的东西。它仍然比让它吃 CPU 周期直到下一次重新启动要好(这是 Windows 服务的一部分)。
@Marc:我是你的忠实粉丝。但是,这一次,我想知道,为什么你没有使用 TheSoftwareJedi 提到的 result.AsyncWaitHandle。在 AsyncWaitHandle 上使用 ManualResetEvent 有什么好处?
@Anand 好吧,这是几年前的事了,所以我无法从记忆中回答 - 但“易于理解”在线程代码中很重要
G
George Tsiokos

对 Pop Catalin 的最佳答案的一些小改动:

函数而不是动作

超时值错误引发异常

在超时的情况下调用 EndInvoke

添加了重载以支持信号工作者取消执行:

public static T Invoke<T> (Func<CancelEventArgs, T> function, TimeSpan timeout) {
    if (timeout.TotalMilliseconds <= 0)
        throw new ArgumentOutOfRangeException ("timeout");

    CancelEventArgs args = new CancelEventArgs (false);
    IAsyncResult functionResult = function.BeginInvoke (args, null, null);
    WaitHandle waitHandle = functionResult.AsyncWaitHandle;
    if (!waitHandle.WaitOne (timeout)) {
        args.Cancel = true; // flag to worker that it should cancel!
        /* •————————————————————————————————————————————————————————————————————————•
           | IMPORTANT: Always call EndInvoke to complete your asynchronous call.   |
           | http://msdn.microsoft.com/en-us/library/2e08f6yc(VS.80).aspx           |
           | (even though we arn't interested in the result)                        |
           •————————————————————————————————————————————————————————————————————————• */
        ThreadPool.UnsafeRegisterWaitForSingleObject (waitHandle,
            (state, timedOut) => function.EndInvoke (functionResult),
            null, -1, true);
        throw new TimeoutException ();
    }
    else
        return function.EndInvoke (functionResult);
}

public static T Invoke<T> (Func<T> function, TimeSpan timeout) {
    return Invoke (args => function (), timeout); // ignore CancelEventArgs
}

public static void Invoke (Action<CancelEventArgs> action, TimeSpan timeout) {
    Invoke<int> (args => { // pass a function that returns 0 & ignore result
        action (args);
        return 0;
    }, timeout);
}

public static void TryInvoke (Action action, TimeSpan timeout) {
    Invoke (args => action (), timeout); // ignore CancelEventArgs
}

Invoke(e => { // ... if (error) e.Cancel = true; return 5; }, TimeSpan.FromSeconds(5));
值得指出的是,在这个答案中,“超时”方法会一直运行,除非它可以被修改为在标记为“取消”时礼貌地选择退出。
David,这就是专门创建 CancellationToken 类型 (.NET 4.0) 来解决的问题。在这个答案中,我使用了 CancelEventArgs 以便工作人员可以轮询 args.Cancel 以查看它是否应该退出,尽管这应该使用 .NET 4.0 的 CancellationToken 重新实现。
一个使用说明让我困惑了一会儿:如果你的函数/动作代码在超时后可能抛出异常,你需要两个 try/catch 块。您需要围绕调用 Invoke 进行一次尝试/捕获以捕获 TimeoutException。你需要在你的函数/动作中捕获和吞下/记录在你的超时引发后可能发生的任何异常。否则,应用程序将因未处理的异常而终止(我的用例是在比 app.config 中指定的更严格的超时时间上 ping 测试 WCF 连接)
绝对 - 由于函数/动作中的代码可以抛出,它必须在 try/catch 中。按照惯例,这些方法不会尝试尝试/捕获函数/动作。捕获并丢弃异常是一个糟糕的设计。与所有异步代码一样,由方法的用户来尝试/捕获。
P
Pop Catalin

这就是我的做法:

public static class Runner
{
    public static void Run(Action action, TimeSpan timeout)
    {
        IAsyncResult ar = action.BeginInvoke(null, null);
        if (ar.AsyncWaitHandle.WaitOne(timeout))
            action.EndInvoke(ar); // This is necesary so that any exceptions thrown by action delegate is rethrown on completion
        else
            throw new TimeoutException("Action failed to complete using the given timeout!");
    }
}

这不会停止执行任务
并非所有任务都可以安全停止,各种问题都可能到来,死锁、资源泄漏、状态损坏……一般情况下不应该这样做。
J
Jason Jackson

我现在刚刚把它淘汰了,所以它可能需要一些改进,但会做你想做的。这是一个简单的控制台应用程序,但演示了所需的原则。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;


namespace TemporalThingy
{
    class Program
    {
        static void Main(string[] args)
        {
            Action action = () => Thread.Sleep(10000);
            DoSomething(action, 5000);
            Console.ReadKey();
        }

        static void DoSomething(Action action, int timeout)
        {
            EventWaitHandle waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
            AsyncCallback callback = ar => waitHandle.Set();
            action.BeginInvoke(callback, null);

            if (!waitHandle.WaitOne(timeout))
                throw new Exception("Failed to complete in the timeout specified.");
        }
    }

}

好的。我唯一要补充的是,他可能更喜欢抛出 System.TimeoutException 而不仅仅是 System.Exception
哦,是的:我也会把它包装在它自己的类中。
佚名

使用 Thread.Join(int timeout) 怎么样?

public static void CallWithTimeout(Action act, int millisecondsTimeout)
{
    var thread = new Thread(new ThreadStart(act));
    thread.Start();
    if (!thread.Join(millisecondsTimeout))
        throw new Exception("Timed out");
}

这将通知调用方法有问题,但不会中止有问题的线程。
我不确定这是否正确。从文档中不清楚当加入超时过去时工作线程会发生什么。