Implementar tempo limite genérico em C #

157

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

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

Estou procurando uma solução que possa ser implementada de maneira elegante em muitos lugares onde meu código interage com um código temperamental (que não posso mudar).

Além disso, eu gostaria que o código de "tempo limite excedido" não fosse mais executado, se possível.

chilltemp
fonte
46
Apenas um lembrete para quem vê as respostas abaixo: Muitos deles usam o Thread.Abort, o que pode ser muito ruim. Leia os vários comentários sobre isso antes de implementar o Abort no seu código. Pode ser apropriado em algumas ocasiões, mas essas são raras. Se você não entender exatamente o que o Abort faz ou não precisa, implemente uma das soluções abaixo que não o utiliza. São as soluções que não têm tantos votos porque não atenderam às necessidades da minha pergunta.
Chilltemp
Obrigado pelo aviso. +1 voto.
QueueHammer
7
Para obter detalhes sobre os perigos do segmento, leia este artigo de Eric Lippert: blogs.msdn.com/b/ericlippert/archive/2010/02/02/22/…
JohnW

Respostas:

95

A parte realmente complicada aqui foi acabar com a tarefa de longa execução, passando o thread do executor da Action de volta para um local em que pudesse ser abortado. Eu consegui isso com o uso de um delegado empacotado que distribui o thread para matar uma variável local no método que criou o lambda.

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

}
TheSoftwareJedi
fonte
3
Por que a captura (ThreadAbortException)? AFAIK, você não pode realmente capturar uma ThreadAbortException (ela será mostrada novamente depois que o bloco catch for deixado).
Csgero
12
Thread.Abort () é muito perigoso de usar, não deve ser usado com código regular, apenas o código que é garantido como seguro deve ser abortado, como o código Cer.Safe, usa regiões de execução restritas e identificadores seguros. Não deve ser feito para nenhum código.
Pop Catalin
12
Enquanto Thread.Abort () é ruim, não é tão ruim quanto um processo que fica fora de controle e usa todos os ciclos da CPU e bytes de memória que o PC possui. Mas você tem razão em apontar os problemas em potencial para qualquer pessoa que ache esse código útil.
Chilltemp
24
Não acredito que essa seja a resposta aceita, alguém não deve estar lendo os comentários aqui ou a resposta foi aceita antes dos comentários e essa pessoa não verifica sua página de respostas. Thread.Abort não é uma solução, é apenas mais um problema que você precisa resolver!
Lasse V. Karlsen
18
Você é quem não está lendo os comentários. Como o chilltemp diz acima, ele está chamando código sobre o qual NÃO tem controle - e deseja que ele seja interrompido. Ele não tem outra opção além de Thread.Abort () se ele desejar que isso seja executado em seu processo. Você está certo que o Thread.Abort é ruim - mas, como diz chilltemp, outras coisas são piores!
TheSoftwareJedi
73

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

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

A implementação é de código aberto, funciona com eficiência mesmo em cenários de computação paralela e está disponível como parte das Bibliotecas Compartilhadas 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);
    }
}

Este código ainda está com erros, 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<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());
      }
   }

Existe uma condição de corrida. É claramente possível que um ThreadAbortException seja gerado após o método WaitFor<int>.Run()ser chamado. Não encontrei uma maneira confiável de corrigir isso, no entanto, com o mesmo teste, não consigo reproduzir nenhum problema com a resposta aceita pelo TheSoftwareJedi .

insira a descrição da imagem aqui

Rinat Abdullin
fonte
3
É o que eu implementei, ele pode lidar com parâmetros e retornar valor, o que eu prefiro e preciso. Obrigado Rinat
Gabriel Mongeon
7
o que é [imutável]?
raklos
2
Apenas um atributo que usamos para marcar classes imutáveis (imutabilidade é verificada por Mono Cecil em testes de unidade)
Rinat Abdullin
9
Este é um impasse esperando para acontecer (estou surpreso que você ainda não tenha observado). Sua chamada para allowedThread.Abort () está dentro de um bloqueio, que também precisa ser adquirido no bloco final. Isso significa que, enquanto o bloco finalmente está aguardando o bloqueio (porque o watchThread o mantém entre Wait () retornando e Thread.Abort ()), a chamada allowedThread.Abort () também bloqueará indefinidamente a espera do término final (que nunca irá). Therad.Abort () pode bloquear, se uma região protegida de código está em execução - causando bloqueios, ver - msdn.microsoft.com/en-us/library/ty8d3wta.aspx
trickdev
1
trickdev, muito obrigado. Por alguma razão, a ocorrência de impasse parece ser muito pouco frequente, mas temos fixa o código, no entanto :-)
Joannes Vermorel
15

Bem, você pode fazer as coisas com os delegados (BeginInvoke, com um retorno de chamada configurando um sinalizador - e o código original aguardando 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 segmento é perigoso ... então eu não acho que exista uma maneira fácil de fazer isso com robustez.

Vou postar isso, mas observe que não é o ideal - ele não interrompe a tarefa de longa execução e não limpa adequadamente 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<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();
        }
    }
Marc Gravell
fonte
2
Estou perfeitamente feliz matando algo que me chamou atenção. Ainda é melhor do que deixá-lo consumir ciclos de CPU até a próxima reinicialização (isso faz parte de um serviço do Windows).
Chilltemp
@ Marc: Eu sou um grande fã seu. Mas, desta vez, eu me pergunto, por que você não usou result.AsyncWaitHandle, conforme mencionado por TheSoftwareJedi. Algum benefício de usar ManualResetEvent sobre AsyncWaitHandle?
Anand Patel
1
@Anand bem, isso foi há alguns anos, então não posso responder a partir da memória -, mas "fácil de entender" conta muito no código da thread
Marc Gravell
13

Algumas pequenas mudanças na grande resposta de Pop Catalin:

  • Func em vez de Ação
  • Lançar exceção no valor de tempo limite ruim
  • Chamar EndInvoke em caso de tempo limite

Sobrecargas foram adicionadas para dar suporte ao trabalhador de sinalização para cancelar a execução:

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
}
George Tsiokos
fonte
Invoque (e => {// ... if (erro) e.Cancel = true; retorne 5;}, TimeSpan.FromSeconds (5));
George Tsiokos
1
Vale ressaltar que nesta resposta o método 'tempo limite esgotado' permanece em execução, a menos que possa ser modificado para educadamente optar por sair quando sinalizado com 'cancelar'.
David Eison
David, é para isso que o tipo CancellationToken (.NET 4.0) foi criado especificamente para resolver. Nesta resposta, usei CancelEventArgs para que o trabalhador pudesse pesquisar args.Cancel para ver se deveria sair, embora isso deva ser reimplementado com o CancellationToken for .NET 4.0.
George Tsiokos
Uma observação sobre isso que me confundiu por um tempo: Você precisa de dois blocos try / catch se o seu código de Função / Ação pode lançar uma exceção após o tempo limite. Você precisa de uma tentativa / captura na chamada de Invoke para capturar TimeoutException. Você precisa de um segundo em sua Função / Ação para capturar e engolir / registrar qualquer exceção que possa ocorrer após o término do tempo limite. Caso contrário, o aplicativo vai terminar com uma excepção não processada (meu caso de uso é de ping testando uma ligação WCF sobre um tempo de espera mais apertado que o especificado em app)
fiat
Absolutamente - como o código dentro da função / ação pode ser lançado, ele deve estar dentro de uma tentativa / captura. Por convenção, esses métodos não tentam / capturam a função / ação. É um projeto ruim para capturar e jogar fora a exceção. Como em todo código assíncrono, cabe ao usuário do método tentar / capturar.
George Tsiokos 19/10/12
10

É 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!");
    }
}
Pop Catalin
fonte
3
isso não impede que a tarefa de executar
TheSoftwareJedi
2
Nem todas as tarefas são seguras para parar, todos os tipos de problemas podem chegar, conflitos, vazamento de recursos, corrupção de estado ... Isso não deve ser feito no caso geral.
Pop Catalin
7

Eu acabei de nocautear agora, então pode precisar de algumas melhorias, mas fará 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.");
        }
    }

}
Jason Jackson
fonte
1
Agradável. A única coisa que eu gostaria de acrescentar é que ele pode preferir jogar System.TimeoutException ao invés de apenas System.Exception
Joel Coehoorn
Ah, sim: e eu envolvia isso na sua própria classe também.
Joel Coehoorn
2

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

fonte
1
Isso notificaria o método de chamada de um problema, mas não abortaria o encadeamento ofensivo.
chilltemp
1
Não sei se isso está correto. Não está claro na documentação o que acontece com o encadeamento de trabalho quando o tempo limite de junção termina.
Matthew Lowe