实现 C # 通用超时

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

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

我正在寻找一个解决方案,可以在我的代码与喜怒无常的代码(我不能改变)交互的许多地方优雅地实现。

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

73712 次浏览

您可以使用委托(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();
}
}

我刚刚把这个弄出来了,所以它可能需要一些改进,但是你想怎么做就怎么做。这是一个简单的控制台应用程序,但演示了所需的原则。

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.");
}
}


}

这里真正棘手的部分是通过将执行器线程从 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();
}
}


}

我是这么做的:

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!");
}
}

对于流行歌手卡塔林的伟大回答,有一些小小的改动:

  • 玩乐而不是行动
  • 对错误的超时值引发异常
  • 在超时的情况下调用 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
}

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

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

实现是开源的,即使在并行计算场景中也能高效工作,并且可以作为 Lokad 共享图书馆的一部分使用

/// <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。我没有找到一个可靠的方法来解决这个问题,但是用同样的测试,我不能复制任何问题与 软件绝地接受的答案。

enter image description here

使用 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");
}