C # Genel Zaman Aşımı Uygula


157

Tek bir satır (veya anonim delege) bir zaman aşımı ile yürütmek için genel bir yol uygulamak için iyi fikirler arıyorum.

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

Ben zarif benim kod mizaç kodu ile etkileşen birçok yerde (ki ben değiştiremezsiniz) uygulanabilir bir çözüm arıyorum.

Buna ek olarak, eğer mümkünse daha fazla yürütme durdurdu rahatsız edici "zaman aşımına uğradı" kodu olmasını istiyorum.


46
Aşağıdaki cevaplara bakan herkese bir hatırlatma: Birçoğu çok kötü olabilecek Thread.Abort kullanıyor. Lütfen kodunuza Abort uygulamadan önce bununla ilgili çeşitli yorumları okuyun. Bazen uygun olabilir, ancak bunlar nadirdir. Abort'un ne yaptığını tam olarak bilmiyorsanız veya buna ihtiyacınız yoksa, lütfen kullanmadığınız çözümlerden birini uygulayın. Çok fazla oyu olmayan çözümler, çünkü sorumun ihtiyaçlarına uymadılar.
chilltemp

Danışma için teşekkürler. +1 oy.
QueueHammer

7
İpliğin tehlikeleri hakkında ayrıntılar için, Eric Lippert'in
JohnW 14:11

Yanıtlar:


95

Buradaki gerçekten zor olan kısım, yürütücü iş parçacığını Eylem'den iptal edilebilecek bir yere geri göndererek uzun süren görevi öldürmekti. Bunu lambda'yı oluşturan yöntemde yerel bir değişkene öldürmek için ipliği geçiren sarılı bir delege kullanarak başardım.

Bu örneği zevk için sunuyorum. Gerçekten ilgilendiğiniz yöntem CallWithTimeout. Bu işlem uzun süren iş parçacığını iptal ederek ve ThreadAbortException'ı yutarak iptal eder :

Kullanımı:

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

İşi yapan statik yöntem:

    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
Neden catch (ThreadAbortException)? AFAIK gerçekten bir ThreadAbortException yakalayamazsınız (catch bloğu bırakıldıktan sonra yeniden yazılacaktır).
csgero

12
Thread.Abort () kullanımı çok tehlikelidir, Normal kodla kullanılmamalıdır, Cer.Safe kodu gibi kısıtlı yürütme bölgeleri ve güvenli tanıtıcılar gibi yalnızca güvenli olduğu garanti edilen kod iptal edilmelidir. Herhangi bir kod için yapılmamalıdır.
Pop Catalin

12
Thread.Abort () kötü olsa da, kontrolden çıkan ve bilgisayarın sahip olduğu her CPU döngüsünü ve baytını kullanan bir süreç kadar kötü değildir. Ancak, bu kodun yararlı olduğunu düşünebilecek herkese olası sorunları belirtme hakkınız vardır.
08:37

24
Bunun kabul edilen cevap olduğuna inanamıyorum, birisi buradaki yorumları okumamalı veya cevaplar yorumlardan önce kabul edildi ve bu kişi cevaplar sayfasını kontrol etmiyor. Sapma bir çözüm değil, çözmeniz gereken başka bir sorun!
Lasse V. Karlsen

18
Yorumları okumayan sensin. Chilltemp'in yukarıda söylediği gibi, kod üzerinde HİÇ kontrolü yok - ve iptal etmesini istiyor. İşlemi içinde çalışmasını istiyorsa Thread.Abort () dışında bir seçeneği yoktur. Thread.Abort kötü - haklısın - ama chilltemp'in dediği gibi, diğer şeyler daha kötü!
TheSoftwareJedi

73

Biz bu tür kodu ağırlıklı olarak productio n kullanıyoruz:

var result = WaitFor<Result>.Run(1.Minutes(), () => service.GetSomeFragileResult());

Uygulama açık kaynaklıdır, paralel hesaplama senaryolarında bile verimli çalışır ve Lokad Paylaşılan Kitaplıklarının bir parçası olarak kullanılabilir

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

Bu kod hala buggy, bu küçük test programı ile deneyebilirsiniz:

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

Bir yarış durumu var. Yöntem WaitFor<int>.Run()çağrıldıktan sonra bir ThreadAbortException özel durumu oluşturulabilir. Bunu düzeltmek için güvenilir bir yol bulamadım, ancak aynı testle TheSoftwareJedi kabul edilen cevapla ilgili herhangi bir sorunu yeniden oluşturamıyorum .

resim açıklamasını buraya girin


3
Uyguladığım bu, Tercih ettiğim ve ihtiyaç duyduğum parametreleri ve dönüş değerini işleyebilir. Teşekkürler Rinat
Gabriel Mongeon

7
[Değişmez] nedir?
raklos

2
Değişmez sınıfları işaretlemek için kullandığımız bir özellik (değişmezlik, birim testlerde Mono Cecil tarafından doğrulandı)
Rinat Abdullin

9
Bu gerçekleşmesini bekleyen bir kilitlenme (henüz gözlemlemediğiniz için şaşırdım). WatchedThread.Abort () yöntemine çağrınız, nihayet bloğunda da edinilmesi gereken bir kilit içinde. Bu, sonunda blok kilidi beklerken (watchedThread öğesinin Wait () dönen ile Thread.Abort ()) arasında olması nedeniyle, watchedThread.Abort () çağrısının da sonunda bitmesini ( asla olmaz). Therad.Abort () kod korumalı bir bölge çalışıyorsa engellenebilir - kilitlenmelere neden olabilir, bkz. - msdn.microsoft.com/en-us/library/ty8d3wta.aspx
trickdev

1
trickdev, çok teşekkürler. Nedense, kilitlenme oluşumu çok seyrek gibi görünüyor, ama yine de kodu düzelttik :-)
Joannes Vermorel

15

Peki, delegelerle (BeginInvoke, bir bayrak ayarlayan geri arama ve bu bayrağı veya zaman aşımını bekleyen orijinal kodu) bir şeyler yapabilirsiniz, ancak sorun çalışan kodu kapatmanın çok zor olmasıdır. Örneğin, bir iş parçacığını öldürmek (veya duraklatmak) tehlikelidir ... bu yüzden bunu sağlam bir şekilde yapmanın kolay bir yolu olduğunu düşünmüyorum.

Bunu göndereceğim, ancak ideal olmadığını unutmayın - uzun süren görevi durdurmaz ve başarısızlık durumunda düzgün bir şekilde temizlenmez.

    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
Üzerime düşen bir şeyi öldürmekten son derece mutluyum. Bir sonraki yeniden başlatmaya kadar CPU döngülerini yemesine izin vermekten daha iyidir (bu bir Windows hizmetinin bir parçasıdır).
09:09

@Marc: Ben senin büyük hayranınım. Ama bu sefer merak ediyorum, neden sonucu kullanmadım. TheSoftwareJedi tarafından belirtildiği gibi AsyncWaitHandle. AsyncWaitHandle üzerinde ManualResetEvent kullanmanın herhangi bir faydası var mı?
Anand Patel

1
@Ayrıca, bu birkaç yıl önceydi, bu yüzden bellekten cevap veremiyorum - ancak "kolay anlaşılır", dişli kodda çok şey sayar
Marc Gravell

13

Pop Catalin'in büyük cevabında bazı küçük değişiklikler:

  • Eylem yerine işlev
  • Hatalı zaman aşımı değerinde istisna atma
  • Zaman aşımı durumunda EndInvoke çağrısı

Yürütmeyi iptal etmek için sinyal çalışanını desteklemek üzere aşırı yükler eklendi:

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
}

Çağırın (e => {// ... if (hata) e.Cancel = true; 5 döndür;}, TimeSpan.FromSeconds (5));
George Tsiokos

1
Bu cevapta, 'iptal' ile işaretlendiğinde kibarca çıkmayı seçecek şekilde modifiye edilemediği sürece 'zaman aşımına uğradı' yönteminin çalışmaya devam ettiğini belirtmek gerekir.
David Eison

David, CancellationToken türünün (.NET 4.0) özel olarak adresleme için oluşturulduğu şey budur. Bu cevapta, işçi args yoklamak için CancelEventArgs kullandım.Çıkış olup olmadığını görmek için iptal edin, ancak bu .NET 4.0 için CancellationToken ile yeniden uygulanmalıdır.
George Tsiokos

Bu konuda beni biraz şaşırtan bir kullanım notu: İşlev / Eylem kodunuz zaman aşımından sonra bir istisna atabiliyorsa iki deneme / yakalama bloğuna ihtiyacınız vardır. TimeoutException'ı yakalamak için Çağrıyı çağırmak için tek bir deneme / yakalama gerekir. Zaman aşımı sürenizden sonra oluşabilecek herhangi bir istisnayı yakalamak ve yutmak / günlüğe kaydetmek için İşlev / İşleminizin içinde bir saniyeye ihtiyacınız vardır. Aksi takdirde uygulama işlenmemiş bir istisna ile sona erer (benim kullanım durumum, bir WCF bağlantısını app.config dosyasında belirtilenden daha sıkı bir zaman aşımı üzerinde test etmektir)
fiat

Kesinlikle - fonksiyonun / eylemin içindeki kod atılabileceğinden, bir dene / yakala içinde olmalıdır. Kural olarak, bu yöntemler işlevi / eylemi denemeye / yakalamaya çalışmaz. İstisnayı yakalamak ve atmak kötü bir tasarım. Tüm eşzamansız kodlarda olduğu gibi, deneme / yakalama yönteminin kullanıcısına kalmış.
George Tsiokos

10

Ben böyle yapardım:

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
bu yürütme görevini durdurmaz
TheSoftwareJedi

2
Tüm görevlerin durması güvenli değildir, her türlü sorun gelebilir, kilitlenmeler, kaynak kaçağı, devletin bozulması ... Genel durumda yapılmamalıdır.
Pop Catalin

7

Ben şimdi bunu nakavt ettik, bu yüzden biraz iyileştirmeye ihtiyaç duyabilir, ama ne istersen yapacak. Basit bir konsol uygulamasıdır, ancak gereken ilkeleri gösterir.

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
Güzel. Ekleyeceğim tek şey, sadece System.Exception yerine System.TimeoutException'ı atmayı tercih edebileceğidir
Joel Coehoorn

Oh, evet: ve bunu kendi sınıfına da sararım.
Joel Coehoorn

2

Thread.Join (int timeout) kullanmaya ne dersiniz?

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
Bu, çağırma yöntemini bir sorunla ilgili bilgilendirir, ancak rahatsız edici iş parçacığını iptal etmez.
Ocak'ta chilltemp

1
Bunun doğru olduğundan emin değilim. Birleştirme zaman aşımı süresi dolduğunda, çalışan iş parçacığına ne olduğu belgelere açık değildir.
Matthew Lowe
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.