Реализация общего тайм-аута C #

157

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

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

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

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

chilltemp
источник
46
Просто напоминание всем, кто ищет ответы ниже: Многие из них используют Thread.Abort, что может быть очень плохо. Пожалуйста, прочтите различные комментарии по этому поводу, прежде чем применять Abort в своем коде. Это может быть уместно в некоторых случаях, но это редко. Если вы не совсем понимаете, что делает Abort, или не нуждаетесь в этом, пожалуйста, используйте одно из нижеприведенных решений, которое его не использует. Это решения, у которых не так много голосов, потому что они не соответствуют потребностям моего вопроса.
Chilltemp
Спасибо за консультацию. +1 голос.
QueueHammer
7
Подробнее об опасностях темы. Об этом читайте в статье Эрика Липперта: blogs.msdn.com/b/ericlippert/archive/2010/02/22/…
JohnW

Ответы:

95

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

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

}
TheSoftwareJedi
источник
3
Почему подвох (ThreadAbortException)? На самом деле, вы не можете поймать исключение ThreadAbortException (оно будет переброшено после того, как блок catch оставлен).
csgero
12
Thread.Abort () очень опасен в использовании, его нельзя использовать с обычным кодом, должен быть прерван только код, который гарантированно безопасен, такой как код Cer.Safe, использующий ограниченные области выполнения и безопасные дескрипторы. Это не должно быть сделано для любого кода.
Поп Каталин
12
Несмотря на то, что Thread.Abort () плох, он далеко не так плох, как процесс, выходящий из-под контроля и использующий каждый цикл ЦП и байт памяти, которые есть у ПК. Но вы правы, указывая на потенциальные проблемы для всех, кто считает этот код полезным.
Chilltemp
24
Я не могу поверить, что это принятый ответ, кто-то не должен читать комментарии здесь, или ответ был принят до комментариев, и этот человек не проверяет свою страницу ответов. Thread.Abort - это не решение, это просто еще одна проблема, которую вам нужно решить!
Лассе В. Карлсен
18
Вы тот, кто не читает комментарии. Как говорит Chilltemp выше, он вызывает код, который не имеет никакого контроля - и хочет, чтобы он прерывался. У него нет другого выбора, кроме Thread.Abort (), если он хочет, чтобы это выполнялось в его процессе. Вы правы, что Thread.Abort - это плохо, но, как говорит Chilltemp, другие вещи хуже!
TheSoftwareJedi
73

Мы используем такой код в производстве :

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

Есть состояние гонки. Вполне возможно, что ThreadAbortException вызывается после вызова метода WaitFor<int>.Run(). Я не нашел надежного способа исправить это, однако с помощью того же теста я не могу воспроизвести любую проблему с принятым ответом TheSoftwareJedi .

введите описание изображения здесь

Ринат Абдуллин
источник
3
Это то, что я реализовал, он может обрабатывать параметры и возвращаемое значение, которое я предпочитаю и нужно. Спасибо Ринат
Габриэль Монгеон
7
что такое [неизменный]?
Раклос
2
Просто атрибут, который мы используем для маркировки неизменяемых классов (неизменность проверяется Моно Сесилом в модульных тестах)
Ринат Абдуллин
9
Это тупик, ожидающий, чтобы случиться (я удивлен, что Вы еще не наблюдали это). Ваш вызов watchedThread.Abort () находится внутри блокировки, которую также необходимо получить в блоке finally. Это означает, что пока блок finally ожидает блокировки (так как watchedThread имеет ее между возвратом Wait () и Thread.Abort ()), вызов watchedThread.Abort () также будет блокироваться на неопределенный срок, ожидая завершения finally (которое никогда не буду). Therad.Abort () может блокировать, если защищенная область кода работает - вызывая взаимоблокировки, см. - msdn.microsoft.com/en-us/library/ty8d3wta.aspx
trickdev
1
Трикдев, спасибо большое. По какой-то причине возникновение взаимоблокировки кажется очень редким, но мы, тем не менее, исправили код :-)
Joannes Vermorel
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
@Marc: Я твой большой поклонник. Но на этот раз мне интересно, почему вы не использовали результат. AsyncWaitHandle, как упомянуто TheSoftwareJedi. Любое преимущество использования ManualResetEvent над AsyncWaitHandle?
Ананд Патель
1
@ И хорошо, это было несколько лет назад, поэтому я не могу ответить по памяти - но «легкий для понимания» имеет большое значение в многопоточном коде
Марк Гравелл
13

Некоторые незначительные изменения в великолепном ответе Поп-Каталина:

  • Func вместо Action
  • Сгенерировать исключение при неверном значении тайм-аута
  • Вызов 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
}
Георгий Циокос
источник
Invoke (e => {// ... if (error) e.Cancel = true; return 5;}, TimeSpan.FromSeconds (5));
Джордж Циокос,
1
Стоит отметить, что в этом ответе метод «тайм-аут» остается запущенным, если он не может быть изменен для вежливого выбора выхода при пометке «отмена».
Дэвид Эйсон
Дэвид, это то, для чего был специально создан тип CancellationToken (.NET 4.0). В этом ответе я использовал CancelEventArgs, чтобы работник мог опрашивать args.Cancel, чтобы увидеть, должен ли он выйти, хотя это должно быть повторно реализовано с помощью CancellationToken для .NET 4.0.
Джордж Циокос
Замечание по использованию, которое немного смутило меня: вам нужно два блока try / catch, если ваш код функции / действия может вызвать исключение после тайм-аута. Вам нужно сделать одну попытку / перехватить вызов Invoke, чтобы перехватить TimeoutException. Вам нужна секунда в вашей функции / действии, чтобы захватить и проглотить / записать в журнал любое исключение, которое может произойти после вашего истечения времени ожидания. В противном случае приложение завершит работу с необработанным исключением (мой вариант использования - проверка ping-теста на соединение WCF с более коротким временем ожидания, чем указано в app.config)
Fiat
Абсолютно - поскольку код внутри функции / действия может выдать, он должен быть внутри try / catch. По соглашению, эти методы не пытаются попытаться / поймать функцию / действие. Это плохой дизайн, чтобы поймать и выбросить исключение. Как и во всем асинхронном коде, пользователь должен попробовать / перехватить метод.
Джордж Циокос
10

Вот как я это сделаю:

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
это не останавливает выполнение задачи
TheSoftwareJedi
2
Не все задачи можно безопасно остановить, могут возникнуть все виды проблем, взаимоблокировки, утечка ресурсов, искажение состояния ... В общем случае этого делать не следует.
Поп Каталин
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
Ницца. Единственное, что я хотел бы добавить, - это то, что он может предпочесть исключение System.TimeoutException, а не просто System.Exception
Джоэл Коухорн
О, да, и я бы обернул это в своем собственном классе.
Джоэл Коухорн
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");
}

источник
1
Это сообщит вызывающему методу о проблеме, но не прервет нарушающий поток.
Chilltemp
1
Я не уверен, что это правильно. Из документации не ясно, что происходит с рабочим потоком по истечении времени ожидания соединения.
Мэтью Лоу