Реализация общего тайм-аута 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
Подробнее об опасностях темы. Об этом читайте в статье Эрика Липперта: 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();
        }
    }

}

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