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 2010-04-15

자문 해 주셔서 감사합니다. +1 투표.
QueueHammer 2010

7
thread.Abort의 위험성에 대한 자세한 내용은 Eric Lippert의이 기사를 읽으십시오. blogs.msdn.com/b/ericlippert/archive/2010/02/22/…
JohnW

답변:


95

여기서 정말 까다로운 부분은 실행자 스레드를 Action에서 중단 될 수있는 위치로 다시 전달하여 장기 실행 작업을 죽이는 것입니다. 람다를 만든 메서드에서 로컬 변수로 죽이기 위해 스레드를 전달하는 래핑 된 대리자를 사용하여이 작업을 수행했습니다.

즐거움을 위해이 예를 제출합니다. 정말로 관심이있는 메서드는 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
왜 catch (ThreadAbortException)입니까? AFAIK는 실제로 ThreadAbortException을 잡을 수 없습니다 (catch 블록이 남아있을 때 다시 발생합니다).
csgero

12
Thread.Abort ()는 사용하기에 매우 위험합니다. 일반 코드와 함께 사용해서는 안되며, Cer.Safe 코드와 같이 안전한 것으로 보장되는 코드 만 중단되어야하며 제한된 실행 영역과 안전한 핸들을 사용합니다. 모든 코드에 대해 수행해서는 안됩니다.
Pop Catalin

12
Thread.Abort ()가 나쁘지만, 프로세스가 제어 할 수없고 PC가 가지고있는 모든 CPU주기와 메모리를 사용하는 것만 큼 나쁘지는 않습니다. 그러나이 코드가 유용하다고 생각하는 다른 사람에게 잠재적 인 문제를 지적하는 것은 옳습니다.
chilltemp

24
나는 이것이 받아 들여진 대답이라는 것을 믿을 수없고, 누군가 여기에서 코멘트를 읽지 않았거나 코멘트 전에 대답이 받아 들여졌고 그 사람은 그의 대답 페이지를 확인하지 않습니다. Thread.Abort는 해결책이 아니라 해결해야 할 또 다른 문제입니다!
Lasse V. Karlsen

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

경쟁 조건이 있습니다. 메서드 WaitFor<int>.Run()가 호출 된 후에 ThreadAbortException이 발생할 가능성이 분명합니다 . 나는 이것을 고칠 믿을만한 방법을 찾지 못했지만 동일한 테스트로 TheSoftwareJedi가 받아 들인 대답으로 문제를 재현 할 수 없습니다 .

여기에 이미지 설명 입력


3
이것이 내가 구현 한 것은 내가 선호하고 필요한 매개 변수와 반환 값을 처리 할 수 ​​있습니다. 감사합니다 Rinat
Gabriel Mongeon 2010-07-12

7
[불변]은 무엇입니까?
raklos

2
불변 클래스를 표시하는 데 사용하는 속성 일뿐입니다 (불변성은 단위 테스트에서 Mono Cecil에 의해 확인 됨)
Rinat Abdullin

9
이것은 발생하기를 기다리는 교착 상태입니다 (아직 관찰하지 않은 것이 놀랍습니다). watchedThread.Abort ()에 대한 호출은 잠금 안에 있으며 finally 블록에서도 획득해야합니다. 이것은 finally 블록이 잠금을 기다리는 동안 (watchedThread가 Wait () 반환과 Thread.Abort () 사이에 있기 때문에), watchedThread.Abort () 호출은 finally가 끝날 때까지 기다릴 것입니다. 결코). Therad.Abort ()는 보호 된 코드 영역이 실행중인 경우 차단할 수 있습니다-교착 상태를 유발합니다.- msdn.microsoft.com
en

1
trickdev, 감사합니다. 어떤 이유로 교착 상태 발생이 매우 드물게 발생하는 것처럼 보이지만 그럼에도 불구하고 코드를 수정했습니다. :-)
Joannes Vermorel 2011

15

글쎄, 당신은 델리게이트로 일을 할 수 있습니다 (플래그를 설정하는 콜백과 그 플래그 또는 타임 아웃을 기다리는 원래 코드)-문제는 실행중인 코드를 종료하기가 매우 어렵다는 것입니다. 예를 들어 스레드를 죽이는 (또는 일시 중지) 위험합니다 ... 그래서 이것을 강력하게 수행하는 쉬운 방법이 없다고 생각합니다.

나는 이것을 게시 할 것이지만 이상적이지 않다는 점에 유의하십시오. 장기 실행 작업을 중지하지 않으며 실패시 제대로 정리되지 않습니다.

    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
나는 나에게 황량한 무언가를 죽이는 것이 완벽하게 행복합니다. 다음 재부팅까지 CPU주기를 먹게하는 것보다 여전히 낫습니다 (이것은 Windows 서비스의 일부입니다).
chilltemp

@Marc : 나는 당신의 훌륭한 팬입니다. 그러나 이번에는 TheSoftwareJedi에서 언급 한 것처럼 result.AsyncWaitHandle을 사용하지 않은 이유가 궁금합니다. AsyncWaitHandle보다 ManualResetEvent를 사용하면 어떤 이점이 있습니까?
Anand Patel 2011

1
@Anand 글쎄, 이것은 몇 년 전 이었기 때문에 메모리에서 대답 할 수는 없지만 스레드 코드에서는 "이해하기 쉬운"부분이 많다.
Marc Gravell

13

Pop Catalin의 훌륭한 답변에 대한 몇 가지 사소한 변경 사항 :

  • Action 대신 Func
  • 잘못된 시간 초과 값에 예외 발생
  • 시간 초과시 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 (오류) e.Cancel = true; return 5;}, TimeSpan.FromSeconds (5));
George Tsiokos 2011

1
이 답변에서 '취소'로 플래그가 지정되었을 때 종료하도록 정중하게 선택하도록 수정할 수없는 경우 '시간 초과'방법이 계속 실행된다는 점을 지적 할 가치가 있습니다.
데이비드 Eison

David, 그것이 바로 CancellationToken 유형 (.NET 4.0)이 해결하기 위해 만들어진 것입니다. 이 답변에서는 CancelEventArgs를 사용하여 작업자가 args.Cancel을 폴링하여 종료해야하는지 확인할 수 있었지만 .NET 4.0 용 CancellationToken으로 다시 구현해야합니다.
George Tsiokos 2011

잠시 혼란 스러웠던 사용법 참고 : 함수 / 액션 코드가 시간 초과 후 예외를 throw 할 수있는 경우 두 개의 try / catch 블록이 필요합니다. TimeoutException을 잡으려면 Invoke에 대한 호출 주위에 한 번의 try / catch가 필요합니다. 시간 초과가 발생한 후에 발생할 수있는 모든 예외를 캡처하고 삼키고 기록하려면 함수 / 액션 내부에 1 초가 필요합니다. 그렇지 않으면 앱이 처리되지 않은 예외와 함께 종료됩니다 (내 사용 사례는 app.config에 지정된 것보다 더 엄격한 제한 시간에 WCF 연결을 ping 테스트하는 것입니다)
fiat

물론-함수 / 액션 내부의 코드가 throw 될 수 있으므로 try / catch 내부에 있어야합니다. 관례에 따라 이러한 메서드는 함수 / 작업을 시도 / 잡으려고하지 않습니다. 예외를 포착하고 버리는 것은 나쁜 디자인입니다. 모든 비동기 코드와 마찬가지로 시도 / 캐치하는 방법은 사용자에게 달려 있습니다.
George Tsiokos

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
모든 작업을 중지하는 것이 안전하지는 않습니다. 모든 종류의 문제가 도착할 수 있습니다. 교착 상태, 리소스 누출, 상태 손상 등 일반적인 경우에는 수행해서는 안됩니다.
Pop Catalin

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.Exception 대신 System.TimeoutException을 던지는 것을 선호 할 수 있다는 것입니다
Joel Coehoorn

오, 예 : 그리고 그것도 자체 수업으로 포장하겠습니다.
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 2010 년

1
그것이 맞는지 잘 모르겠습니다. 조인 시간 초과가 경과 할 때 작업자 스레드에 어떤 일이 발생하는지 문서에서 명확하지 않습니다.
Matthew Lowe
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.