Реалізуйте загальний тайм-аут C #


157

Я шукаю гарні ідеї для реалізації загального способу виконання єдиного рядка (або анонімного делегата) виконання коду з таймаутом.

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

Я шукаю рішення, яке можна елегантно реалізувати в багатьох місцях, де мій код взаємодіє з температурним кодом (який я не можу змінити).

Крім того, я хотів би, щоб порушений "вичерпаний" код припинився від подальшого виконання, якщо це можливо.


46
Просто нагадування для тих, хто дивиться на відповіді нижче: Багато з них використовують Thread.Abort, що може бути дуже погано. Будь ласка, прочитайте різні коментарі з цього приводу, перш ніж застосувати Abort у своєму коді. Це може бути доречно в деяких випадках, але вони бувають рідко. Якщо ви точно не розумієте, що робить Abort або не потрібно, будь ласка, застосуйте одне з наведених нижче рішень, яке не використовує. Це рішення, які не мають стільки голосів, оскільки вони не відповідали потребам мого питання.
chilltemp

Дякую за дорадчі. +1 голос.
QueueHammer

7
Детальніше про небезпеку потоку. Аборт читайте цю статтю від Еріка Ліпперта
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();
        }
    }

}

3
Чому вилов (ThreadAbortException)? AFAIK ви не можете дійсно зловити ThreadAbortException (він буде повторно скинутий після того, як залишиться блок вилову).
csgero

12
Thread.Abort () є дуже небезпечним у використанні, його не слід використовувати зі звичайним кодом, лише код, який гарантовано є безпечним, повинен бути скасований, наприклад, код, який є Cer.Safe, використовує обмежені регіони виконання та безпечні ручки. Це не слід робити для будь-якого коду.
Поп Каталін

12
Хоча Thread.Abort () поганий, він не є поруч з процесом, який закінчується контролем і використовує кожен цикл процесора та байт пам'яті, який має ПК. Але ви праві вказати на можливі проблеми тим, хто вважає цей код корисним.
chilltemp

24
Я не можу повірити, що це прийнята відповідь, хтось не повинен читати тут коментарі, або відповідь була прийнята перед коментарями, і ця людина не перевіряє свою сторінку відповідей. Thread.Abort - це не рішення, це просто ще одна проблема, яку потрібно вирішити!
Лассе В. Карлсен

18
Ви той, хто не читає коментарів. Як зазначає chilltemp вище, він називає код того, що він НЕ контролює - і хоче, щоб він припинив. У нього немає іншого варіанту, крім Thread.Abort (), якщо він хоче, щоб це запустилося в його процесі. Ви маєте рацію, що Thread.Abort поганий - але, як каже застуда, інші речі гірші!
TheSoftwareJedi

73

Ми широко використовуємо такий код у виробництві n:

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
що таке [Immutable]?
raklos

2
Просто атрибут, який ми використовуємо для позначення непорушних класів (незмінність перевірена Mono Cecil в одиничних тестах)
Rinat Abdullin

9
Це тупик, який чекає, коли це станеться (я здивований, що ви цього ще не спостерігали). Ваш дзвінок до watchThread.Abort () знаходиться всередині блокування, який також потрібно отримати в остаточному блоці. Це означає, що, нарешті, блок очікує блокування (тому що у WatchThread є його між поверненням Wait () та Thread.Abort ()), виклик гледаючогоThread.Abort () також буде блокуватися нескінченно, чекаючи, коли нарешті завершиться (що це ніколи не буде). Therad.Abort () може заблокувати, якщо працює захищена область коду - спричиняючи тупикові місця, див. - msdn.microsoft.com/en-us/library/ty8d3wta.aspx
trickdev

1
trickdev, велике спасибі Чомусь поява тупикових ситуацій дуже рідкісна, але ми все-таки виправили код :-)
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
@Anand, це було кілька років тому, тому я не можу відповісти з пам'яті - але "легко зрозуміти" рахує багато в потоковому коді
Marc Gravell

13

Деякі незначні зміни до чудової відповіді Попа Каталіна:

  • Функція замість дії
  • Виключення для поганого значення тайм-ауту
  • Виклик 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
}

Викликати (e => {// ... якщо (помилка) e.Cancel = true; повернути 5;}, TimeSpan.FromSeconds (5));
Джордж Ціокос

1
Варто зазначити, що в цій відповіді метод "вичерпаний" залишається запущеним, якщо він не може бути змінений, щоб ввічливо вибрати вихід, коли позначено "Скасувати".
Девід Ейсон

Девід, саме для цього було спеціально створено тип CancellationToken (.NET 4.0). У цій відповіді я використав CancelEventArgs, щоб працівник міг опитати args.Cancel, щоб побачити, чи повинен він вийти, хоча це слід повторно реалізувати разом із CancellationToken для .NET 4.0.
Джордж Ціокос

Примітка про використання цього, яка мене трохи збила з пантелику: Вам потрібні два блоки спробу / лову, якщо ваш код функції / дії може викинути виняток після таймауту. Вам потрібна одна спроба / ловити навколо виклику Invoke, щоб отримати TimeoutException. Вам потрібна секунда у Вашій Функції / Дії, щоб захопити та проковтнути / записати будь-який виняток, який може статися після викидання тайм-ауту. Інакше додаток припиняється за допомогою необробленого винятку (мій випадок використання - це тестування підключення WCF на більш чіткий час, ніж зазначено в app.config)
фіат,

Абсолютно - оскільки код всередині функції / дії може кидатись, він повинен знаходитися всередині спробувати. Згідно з умовами, ці методи не намагаються спробувати функцію / дію. Це погана конструкція, щоб зловити і викинути виняток. Як і у випадку з усіма асинхронними кодами, користувач методу повинен спробувати / зловити.
Джордж Ціокос

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
Joel Coehoorn

О, так: і я б також вклав це у свій клас.
Джоел Куехорн

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
Я не впевнений, що це правильно. З документації незрозуміло, що відбувається з робочою ниткою, коли минув тайм-аут приєднання.
Меттью Лоу
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.