C#の一般的なタイムアウトを実装する


157

コードの単一行(または匿名デリゲート)をタイムアウトで実行する一般的な方法を実装するための良いアイデアを探しています。

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

私のコードが気質的なコードと対話する(変更できない)多くの場所でエレガントに実装できるソリューションを探しています。

さらに、可能であれば、問題のある「タイムアウト」コードをさらに実行しないようにしたいと思います。


46
以下の答えを見ている人へのリマインダーです:それらの多くはThread.Abortを使用していますが、これは非常に悪い場合があります。コードにアボートを実装する前に、これに関するさまざまなコメントを読んでください。これは適切な場合もありますが、まれです。Abortが何をするか、またはそれを必要としないことを正確に理解していない場合は、それを使用しない以下のソリューションの1つを実装してください。彼らは私の質問のニーズに合わなかったため、投票数が多くないソリューションです。
chilltemp 2010

助言をありがとう。+1票。
QueueHammer 2010年

7
thread.Abortの危険性の詳細については、Eric Lippertの次の記事を読んでください。blogs.msdn.com/ b
ericlippert

回答:


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
なぜcatch(ThreadAbortException)なのですか?私が知る限り、実際にThreadAbortExceptionをキャッチすることはできません(catchブロックが残された後で再スローされます)。
csgero 2008年

12
Thread.Abort()の使用は非常に危険です。通常のコードでは使用しないでください。安全であることが保証されているコード(Cer.Safeのコードなど)だけを強制終了すると、制約された実行領域と安全なハンドルが使用されます。どのコードに対しても行うべきではありません。
Pop Catalin、

12
Thread.Abort()は悪いですが、プロセスが制御不能になり、PCが持っているすべてのCPUサイクルとメモリのバイトを使用しているプロセスほど悪くはありません。しかし、このコードが有用であると考える誰かに潜在的な問題を指摘するのは正しいことです。
chilltemp 2008年

24
これが承認された回答であるとは信じられません。誰かがここでコメントを読んでいてはいけません。Thread.Abortは解決策ではありません。解決する必要があるもう1つの問題です。
Lasse V. Karlsen、

18
あなたはコメントを読んでいない人です。chilltempが上で述べたように、彼は自分が制御できないコードを呼び出しており、それを中止することを望んでいます。これをプロセス内で実行したい場合は、Thread.Abort()以外のオプションはありません。Thread.Abortが悪いのはあなたの言うとおりです-しかし、chilltempが言うように、他のことはもっと悪いです!
TheSoftwareJedi 2009年

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
ガブリエルMongeon 2010

7
[不変]とは何ですか?
raklos、2011年

2
不変クラスをマークするために使用する単なる属性(不変性は単体テストでMono Cecilによって検証されます)
Rinat Abdullin

9
これは、デッドロックが発生するのを待っている状態です(まだ観察していないことに驚いています)。watchedThread.Abort()への呼び出しはロック内にあり、これもfinallyブロックで取得する必要があります。これは、finallyブロックがロックを待機している間(watchedThreadがWait()を返すとThread.Abort()の間にそれを持っているため)、watchedThread.Abort()呼び出しも、finallyが完了するのを待機するために無期限にブロックすることを意味します(それは決して)。コードの保護された領域が実行されている場合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
私はルージュになってしまった何かを殺すことは完全に幸せです。次の再起動まで、CPUサイクルを食わせるよりもなお良いです(これはWindowsサービスの一部です)。
chilltemp 2008年

@Marc:私はあなたの大ファンです。しかし、今回は、なぜTheSoftwareJediが言及したように、なぜresult.AsyncWaitHandleを使用しなかったのかと思います。AsyncWaitHandleよりもManualResetEventを使用する利点は何ですか?
Anand Patel、2011

1
@アナンドよく、これは数年前だったので、メモリから答えることはできません-しかし、「理解しやすい」は、スレッド化されたコードで多くのことを
意味し

13

Pop Catalinの素晴らしい答えに対するいくつかの小さな変更:

  • アクションの代わりに機能
  • 不正なタイムアウト値で例外をスローする
  • タイムアウトの場合に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));
George Tsiokos

1
この回答では、「タイムアウト」メソッドは、「キャンセル」のフラグが付いたときに丁寧に終了するように変更できない限り、実行されたままであることを指摘する価値があります。
David Eison

David、それは、CancellationToken型(.NET 4.0)が対処するために特別に作成されたものです。この回答では、CancelEventArgsを使用して、ワーカーがargs.Cancelをポーリングして終了するかどうかを確認できるようにしましたが、これは.NET 4.0のCancellationTokenで再実装する必要があります。
George Tsiokos

しばらく混乱したこの使用上の注意:関数/アクションコードがタイムアウト後に例外をスローする場合は、2つのtry / catchブロックが必要です。TimeoutExceptionをキャッチするには、Invokeの呼び出しを1回試行またはキャッチする必要があります。タイムアウトがスローされた後に発生する可能性のあるすべての例外をキャプチャして飲み込み/ログに記録するには、関数/アクション内に2番目が必要です。それ以外の場合、アプリは未処理の例外で終了します(私のユースケースは、app.configで指定されているよりもタイトなタイムアウトでWCF接続をpingテストしています)
fiat

絶対に-関数/アクション内のコードがスローできるため、try / catch内にある必要があります。慣例により、これらのメソッドは関数/アクションを試行/キャッチしようとしません。例外をキャッチして捨てるのは悪い設計です。すべての非同期コードと同様に、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 '18

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、2008年

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
それが正しいかわかりません。結合タイムアウトが経過したときにワーカースレッドがどうなるかは、ドキュメントから明確ではありません。
マシューロウ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.