Implementar Tempo Limite Genérico C #

Eu estou procurando boas idéias para implementar uma maneira genérica de ter uma única linha (ou delegado anônimo) de código executar com um tempo limite.

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

Eu estou procurando uma solução que possa ser elegantemente implementada em muitos lugares onde meu código interage com código temperamental (que eu não posso mudar).

Além disso, gostaria que o código “expirado” ofensivo parasse de ser executado, se possível.

    A parte realmente complicada aqui era matar a tarefa de longa duração passando o encadeamento do executor da Ação de volta para um local onde pudesse ser abortado. Eu consegui isso com o uso de um delegado que envolve o segmento para matar em uma variável local no método que criou o lambda.

    Eu submeto este exemplo, para sua diversão. O método que você está realmente interessado é CallWithTimeout. Isso cancelará o encadeamento de longa duração, abortando-o e engolindo o 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); } 

    O método estático fazendo o trabalho:

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

    Estamos usando código como este fortemente na produção n:

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

    A implementação é de código aberto, funciona de forma eficiente mesmo em cenários de computação paralela e está disponível como parte das Bibliotecas compartilhadas do Lokad

     ///  /// Helper class for invoking tasks with timeout. Overhead is 0,005 ms. ///  /// The type of the result. [Immutable] public sealed class WaitFor { readonly TimeSpan _timeout; ///  /// Initializes a new instance of the  class, /// using the specified timeout for all operations. ///  /// The timeout. public WaitFor(TimeSpan timeout) { _timeout = timeout; } ///  /// Executes the spcified function within the current thread, aborting it /// if it does not complete within the specified timeout interval. ///  /// The function. /// result of the function ///  /// 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. ///  /// if function is null /// if the function does not finish in time  public TResult Run(Func 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); } } } ///  /// Executes the spcified function within the current thread, aborting it /// if it does not complete within the specified timeout interval. ///  /// The timeout. /// The function. /// result of the function ///  /// 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. ///  /// if function is null /// if the function does not finish in time  public static TResult Run(TimeSpan timeout, Func function) { return new WaitFor(timeout).Run(function); } } 

    Este código ainda é buggy, você pode tentar com este pequeno programa de teste:

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

    Existe uma condição de corrida. É claramente possível que um ThreadAbortException seja gerado depois que o método WaitFor.Run() está sendo chamado. Eu não encontrei uma maneira confiável de corrigir isso, no entanto, com o mesmo teste, não posso reproduzir nenhum problema com a resposta aceita pelo TheSoftwareJedi .

    insira a descrição da imagem aqui

    Bem, você poderia fazer coisas com delegates (BeginInvoke, com um retorno de chamada definindo um sinalizador – e o código original esperando por esse sinalizador ou tempo limite) – mas o problema é que é muito difícil desligar o código em execução. Por exemplo, matar (ou pausar) um thread é perigoso … então não acho que haja uma maneira fácil de fazer isso de maneira robusta.

    Vou postar isso, mas note que não é ideal – ele não para a tarefa de longa duração e não é limpo corretamente em caso de falha.

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

    Algumas pequenas alterações à grande resposta do Pop Catalin:

    • Func em vez de Action
    • Ative a exceção no valor de tempo limite inválido
    • Chamando o EndInvoke em caso de tempo limite

    Sobrecargas foram adicionadas para apoiar o trabalhador de sinalização para cancelar a execução:

     public static T Invoke (Func 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 (Func function, TimeSpan timeout) { return Invoke (args => function (), timeout); // ignore CancelEventArgs } public static void Invoke (Action action, TimeSpan timeout) { Invoke (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 } 

    É assim que eu faria:

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

    Eu acabei de tirar isso agora, então talvez precise de alguma melhora, mas vai fazer o que você quiser. É um aplicativo de console simples, mas demonstra os princípios necessários.

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

    Que tal usar 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"); }