实现 C# 通用超时
Posted
技术标签:
【中文标题】实现 C# 通用超时【英文标题】:Implement C# Generic Timeout 【发布时间】:2010-09-22 21:18:38 【问题描述】:我正在寻找实现一种通用方法的好主意,以使单行(或匿名委托)代码执行超时。
TemperamentalClass tc = new TemperamentalClass();
tc.DoSomething(); // normally runs in 30 sec. Want to error at 1 min
我正在寻找一种可以在我的代码与气质代码(我无法更改)交互的许多地方优雅地实施的解决方案。
此外,如果可能的话,我希望停止执行有问题的“超时”代码。
【问题讨论】:
只是提醒任何查看以下答案的人:他们中的许多人使用 Thread.Abort 这可能非常糟糕。在您的代码中实现 Abort 之前,请阅读有关此的各种 cmet。有时它可能是合适的,但这种情况很少见。如果您不完全了解 Abort 的用途或不需要它,请实施以下不使用它的解决方案之一。它们是没有那么多选票的解决方案,因为它们不符合我的问题的需要。 有关 thread.Abort 的危险的详细信息,请阅读 Eric Lippert 的这篇文章:blogs.msdn.com/b/ericlippert/archive/2010/02/22/… 【参考方案1】:好吧,您可以使用委托(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 好吧,这是几年前的事了,所以我无法凭记忆回答——但“易于理解”在线程代码中很重要【参考方案2】:我现在刚刚取消了这个,所以它可能需要一些改进,但会做你想做的。这是一个简单的控制台应用程序,但演示了所需的原则。
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 哦,是的:我也会把它包装在它自己的类中。【参考方案3】:这里真正棘手的部分是通过将执行程序线程从 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();
【讨论】:
为什么会出现 catch(ThreadAbortException)? AFAIK 你不能真正捕捉到 ThreadAbortException (它会在 catch 块离开后重新抛出)。 Thread.Abort() 使用起来非常危险,它不应该与常规代码一起使用,只有保证安全的代码才应该中止,例如 Cer.Safe 的代码,使用受约束的执行区域和安全句柄。任何代码都不应该这样做。 虽然 Thread.Abort() 很糟糕,但它远没有进程失控并使用 PC 拥有的每个 CPU 周期和内存字节那么糟糕。但是您向可能认为此代码有用的任何其他人指出潜在问题是正确的。 我不敢相信这是被接受的答案,一定不是有人在这里阅读 cmets,或者答案在 cmets 之前被接受并且那个人没有检查他的回复页面。 Thread.Abort 不是解决方案,它只是您需要解决的另一个问题! 你是不阅读 cmets 的人。正如上面chilltemp所说,他正在调用他无法控制的代码 - 并希望它中止。如果他希望它在他的进程中运行,他除了 Thread.Abort() 之外别无选择。你说得对,Thread.Abort 很糟糕——但就像chilltemp 所说,其他事情更糟!【参考方案4】:这就是我的做法:
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!");
【讨论】:
这不会停止执行任务 并非所有任务都可以安全停止,各种问题都可能到来,死锁、资源泄漏、状态损坏……一般情况下不应该这样做。【参考方案5】:对 Pop Catalin 的最佳答案的一些小改动:
Func 代替 Action 超时值错误引发异常 在超时情况下调用 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。您需要在 Function/Action 中使用一秒钟来捕获并吞下/记录超时引发后可能发生的任何异常。否则应用程序将因未处理的异常而终止(我的用例是在比 app.config 中指定的更严格的超时时间上 ping 测试 WCF 连接) 绝对 - 因为函数/动作中的代码可以抛出,它必须在 try/catch 中。按照惯例,这些方法不会尝试尝试/捕获函数/动作。捕获和丢弃异常是一个糟糕的设计。与所有异步代码一样,由方法的用户来尝试/捕获。【参考方案6】:我们在生产环境中大量使用这样的代码n:
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="WaitForT"/> 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 接受的答案的任何问题。
【讨论】:
这是我实现的,它可以处理参数和返回值,这是我喜欢和需要的。谢谢瑞纳特 只是我们用来标记不可变类的一个属性(不变性由 Mono Cecil 在单元测试中验证) 这是一个等待发生的死锁(我很惊讶你还没有观察到它)。您对watchedThread.Abort() 的调用位于锁内,也需要在finally 块中获取。这意味着当 finally 块等待锁时(因为 watchThread 在 Wait() 返回和 Thread.Abort() 之间有它),watchThread.Abort() 调用也将无限期地阻塞等待 finally 完成(它永远不会)。 Therad.Abort() 可以阻止代码的受保护区域正在运行 - 导致死锁,请参阅 - msdn.microsoft.com/en-us/library/ty8d3wta.aspx trickdev,非常感谢。出于某种原因,死锁似乎很少发生,但我们仍然修复了代码:-) 如何调用 void 类型的函数?【参考方案7】:使用 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");
【讨论】:
这会通知调用方法有问题,但不会中止有问题的线程。 我不确定这是否正确。从文档中不清楚当 Join 超时过去时工作线程会发生什么。以上是关于实现 C# 通用超时的主要内容,如果未能解决你的问题,请参考以下文章
C# 配合 Easyui DataGrid 实现增删改查 通用模板