2008-11-18 13 views
154

Estoy buscando buenas ideas para implementar una forma genérica para tener una sola línea (o un delegado anónimo) de código ejecutado con un tiempo de espera excedido.Implementa C# Generic Timeout

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

Busco una solución que elegantemente se puede implementar en muchos lugares donde mi código interactúa con código temperamental (que no puedo cambiar).

Además, me gustaría que se detuviera el código de "tiempo de espera excedido" para que no se ejecute aún más si es posible.

+43

Recordatorio para cualquiera que mire las respuestas a continuación: Muchos de ellos usan Thread.Abort, que puede ser muy malo. Lea los diversos comentarios sobre esto antes de implementar Abort en su código. Puede ser apropiado en ocasiones, pero esos son raros. Si no comprende exactamente qué es lo que Abort necesita o no, implemente una de las siguientes soluciones que no lo utilice. Son las soluciones que no tienen tantos votos porque no se ajustaban a las necesidades de mi pregunta. – chilltemp

+0

Gracias por el asesoramiento. +1 voto. – QueueHammer

+6

Para obtener más información sobre los peligros del hilo. Aborto, lea este artículo de Eric Lippert: http://blogs.msdn.com/b/ericlippert/archive/2010/02/22/should-i-specify-a-timeout. aspx – JohnW

Respuesta

93

La parte realmente difícil aquí estaba matando a la tarea de larga ejecución a través de pasar el hilo ejecutor de la acción de nuevo a un lugar en el que podría ser abortado. Logré esto con el uso de un delegado envuelto que pasa el hilo para matar en una variable local en el método que creó el lambda.

Presento este ejemplo para su disfrute. El método que realmente te interesa es CallWithTimeout.Esto cancelará el hilo de larga duración al abortar, y tragar el ThreadAbortException:

Uso:

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

El método estático haciendo el trabajo:

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

¿Por qué la captura (ThreadAbortException)? AFAIK no se puede atrapar realmente una ThreadAbortException (se volverá a lanzar después cuando quede el bloque catch). – csgero

+0

Usted mi amigo, es absolutamente correcto. Podemos ignorarlo en este caso y dejar que el hilo muera.Se reabastecerá el grupo, pero esto es algo a tener en cuenta si se espera que esto suceda mucho. Este * PUEDE SER * un problema de rendimiento. – TheSoftwareJedi

+0

Solo como un lado, no estoy seguro de cómo funciona esto en .NET. En Java tendrías que declarar el objeto Thread como final, lo que evitaría que mute. De alguna manera, los genios de CLR hicieron que esto funcionara. Prestigio. – TheSoftwareJedi

15

Bueno, podría hacer cosas con los delegados (BeginInvoke, con una devolución de llamada configurando un indicador - y el código original esperando ese indicador o tiempo de espera) - pero el problema es que es muy difícil cerrar el código en ejecución. Por ejemplo, matar (o pausar) un hilo es peligroso ... así que no creo que haya una manera fácil de hacerlo de forma robusta.

Voy a publicar esto, pero tenga en cuenta que no es ideal, no detiene la tarea de larga duración y no se limpia correctamente en caso de falla.

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

Estoy muy contento de haber matado a algo que se me ha vuelto loco. Todavía es mejor que dejarlo comer ciclos de CPU hasta el próximo reinicio (esto es parte de un servicio de Windows). – chilltemp

+2

En ese caso, considere generar un AppDomain que puede matar ... –

+2

resultado.AsyncWaitHandle se puede utilizar, no es necesario reiniciar manualmente – TheSoftwareJedi

7

Acabo de noquear esto ahora por lo que podría necesitar alguna mejora, pero hará lo que quiera. Es una aplicación de consola simple, pero demuestra los principios necesarios.

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

Niza. Lo único que agregaría es que preferiría lanzar System.TimeoutException en lugar de simplemente System.Exception –

+0

Oh, sí, y yo también incluiría eso en su propia clase. –

+0

resultado. Se puede utilizar AsyncWaitHandle, no es necesario reiniciar manualmente. – TheSoftwareJedi

9

Así es como yo' d hacerlo:

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

esto no detiene la tarea de ejecución – TheSoftwareJedi

+2

No todas las tareas son seguras, pueden llegar todo tipo de problemas, interbloqueos, pérdida de recursos, corrupción de estado ... No debería hacerse en el caso general. –

13

Algunos cambios menores en P gran respuesta de op Catalin:

  • Func lugar de acción
  • Throw excepción de mal tiempo de espera
  • Calling EndInvoke en caso de tiempo de espera

sobrecargas se han añadido para apoyar a los trabajadores de señalización para cancelar la ejecución :

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 
} 
+0

cualquier llamada de código de muestra ¿Método de invocación? – Kiquenet

+0

Invocar (e => { // ... if (error) e.Cancelar = verdadero; return 5; }, TimeSpan.FromSeconds (5)); –

+1

Vale la pena señalar que en esta respuesta, el método de "tiempo de espera agotado" se deja en ejecución a menos que se pueda modificar para elegir cortésmente salir cuando se marca con 'cancelar'. –

74

Estamos usando código similar esta fuertemente en Productio n:

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

La aplicación es de código abierto, funciona de manera eficiente incluso en escenarios de computación paralela y está disponible como parte de 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); 
    } 
} 
+3

Esto que implementé, puede manejar los parámetros y el valor de retorno, que prefiero y necesito. Gracias Rinat –

+0

Felicitaciones! ¡Esto es hermoso! – sunside

+7

¿Qué es [Inmutable]? – raklos

2

Qué acerca del uso Thread.Join (int se acabó el tiempo)?

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

Eso notificará al método que llama de un problema, pero no abortará el hilo ofensivo. – chilltemp

+1

No estoy seguro de que sea correcto. No está claro en la documentación qué sucede con el hilo del trabajador cuando transcurre el tiempo de espera de la unión. –