2008-11-18 1 views
154

Я ищу хорошие идеи для реализации универсального способа выполнения одной строки (или анонимного делегата) кода с таймаутом.Внедрение C# Общий тайм-аут

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

Я ищу решение, которое элегантно может быть реализовано во многих местах, где мой код взаимодействует с темпераментным кодом (что я не могу изменить).

Кроме того, я хотел бы, чтобы оскорбительный «тайм-аут» код прекратил выполнение, если это было возможно.

ответ

93

Действительно сложная часть здесь убивали долго выполняющиеся задачи путем прохождения исполнителя нити от действий обратно на место, где она может быть прервана. Я выполнил это с помощью завернутого делегата, который передает поток, чтобы убить локальную переменную в методе, который создал лямбда.

Я представляю этот пример для вашего удовольствия. Метод, который вас действительно интересует, - 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(); 
     } 
    } 

} 
+3

Почему catch (ThreadAbortException)? AFAIK вы не можете поймать ThreadAbortException (он будет восстановлен после того, как останется блок catch). – csgero 2008-11-18 16:57:39

+0

Вы, мой друг, абсолютно правы. Мы можем просто игнорировать его в этом случае и позволить потоку умереть. Пул будет пополнен, но это то, что нужно принять во внимание, если это произойдет очень часто. Это * МОЖЕТ БЫТЬ * проблема с производительностью. – TheSoftwareJedi 2008-11-18 17:06:30

15

Ну, вы могли бы сделать что-то с делегатами (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(); 
     } 
    } 
+2

Я совершенно счастлив убить то, что у меня пропало. Его все же лучше, чем позволить ему есть циклы процессора до следующей перезагрузки (это часть службы Windows). – chilltemp 2008-11-18 16:09:12

+2

В этом случае рассмотрите возможность создания AppDomain, который вы можете убить ... – 2008-11-18 16:11:50

+2

result.AsyncWaitHandle можно использовать, не требуется ручной сброс – TheSoftwareJedi 2008-11-18 16:57:48

7

Я просто сбил это сейчас, поэтому может потребоваться некоторое улучшение, но он будет делать то, что вы хотите. Это простое консольное приложение, но демонстрирует необходимые принципы.

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

} 
+1

Nice. Единственное, что я хотел бы добавить, это то, что он может предпочесть бросать System.TimeoutException, а не просто System.Exception – 2008-11-18 16:30:34

+0

О, да: и я бы обернул это в свой класс. – 2008-11-18 16:32:57

+0

.AsyncWaitHandle можно использовать, ручного сброса не требуется. – TheSoftwareJedi 2008-11-18 16:35:23

9

Это, как я d сделать это:

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

Некоторые незначительные c hanges к поп большой ответ Каталин в:

  • Func вместо действий
  • Throw исключение на плохое значение тайм-аута
  • Calling 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 
} 
74

Мы используем co де, как это тяжело в костюмно н:

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); 
    } 
} 
2

Что об использовании 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"); 
} 
Смежные вопросы