2016-01-04 14 views
7

Mam serię bloków kodu, które trwają zbyt długo. Nie potrzebuję żadnej finezji, gdy się nie uda. W rzeczywistości chcę rzucić wyjątek, gdy te bloki trwają zbyt długo i po prostu wypadają z naszej standardowej obsługi błędów. Wolałbym NIE tworzyć metod z każdego bloku (które są jedynymi sugestiami, które widziałem do tej pory), ponieważ wymagałoby to poważnej przeróbki podstawy kodu.Jak utworzyć ogólny obiekt limitu czasu dla różnych bloków kodu?

Oto, co chciałbym JAK utworzyć, jeśli to możliwe.

public void MyMethod(...) 
{ 

... 

    using (MyTimeoutObject mto = new MyTimeoutObject(new TimeSpan(0,0,30))) 
    { 
     // Everything in here must complete within the timespan 
     // or mto will throw an exception. When the using block 
     // disposes of mto, then the timer is disabled and 
     // disaster is averted. 
    } 

... 
} 

Stworzyłem prosty obiekt do tego przy użyciu klasy Timer. (UWAGA dla tych, którzy lubią kopiuj/wklej ten kod: NIE DZIAŁA !! NIE)

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Timers; 

    public class MyTimeoutObject : IDisposable 
    { 
     private Timer timer = null; 

     public MyTimeoutObject (TimeSpan ts) 
     { 
      timer = new Timer(); 
      timer.Elapsed += timer_Elapsed; 
      timer.Interval = ts.TotalMilliseconds; 

      timer.Start(); 
     } 

     void timer_Elapsed(object sender, ElapsedEventArgs e) 
     { 
      throw new TimeoutException("A code block has timed out."); 
     } 

     public void Dispose() 
     { 
      if (timer != null) 
      { 
       timer.Stop(); 
      } 
     } 
    } 

To nie działa ponieważ wychwytuje klasy System.Timers.Timer, absorbuje i ignoruje wszelkie wyjątki generowane wewnątrz, który - jak już odkryłem - pokonuje mój projekt. Każdy inny sposób tworzenia tej klasy/funkcjonalności bez całkowitego przeprojektowania?

Wydaje się to takie proste dwie godziny temu, ale powoduje wiele bólu głowy.

+0

Czy zajmujesz się wyjątkami na wyższym poziomie, skąd wywołujesz tę metodę? Co więcej, czy jest to środowisko wielowątkowe? – Shaharyar

+0

Możesz użyć statycznego magazynu 'CancellationToken' i używać go wszędzie tam, gdzie masz zadanie, lub - mniej elegancko, ale może bardziej efektywnie - masz inny proces, który może zabić twój główny proces i jest wywoływany przez lokalny HTTP lub potoki nazwane –

+0

Shaharyar: Tak. Połów jest daleko w górę łańcucha. I tak, jest to środowisko wielowątkowe. Biblioteka, którą piszę, ma być używana w aplikacjach WinForm, a nie w sieci. – Jerry

Odpowiedz

2

OK, spędziłem trochę czasu nad tym i myślę, że mam rozwiązanie, które zadziała dla ciebie bez konieczności zmiany kodu.

Poniżej opisano sposób, w jaki utworzono klasę Timebox.

Oto jak działa Timebox.

  • Timebox obiekt jest tworzony z dana Timespan
  • Kiedy Execute się nazywa, Timebox tworzy dziecko AppDomain posiadać odniesienie TimeboxRuntime obiektu i zwraca pełnomocnictwa do niego
  • The TimeboxRuntime obiekt w dziecko AppDomain pobiera Action jako dane wejściowe do wykonania w domenie podrzędnej
  • Timebox następnie tworzy zadanie wywoływania serwera proxy TimeboxRuntime y
  • Zadanie rozpoczyna się (i wykonanie akcja zaczyna), a „głównym” wątek czeka na tak długo, jak dany TimeSpan
  • po podanej TimeSpan (lub po zakończeniu wykonywania zadania), dziecko AppDomain jest rozładowane, czy zostało ukończone Action, czy nie.
  • TimeoutException zostaje wyrzucony jeśli action czasy się, w przeciwnym razie, jeśli action zgłasza wyjątek, to zostanie złapany przez dziecko AppDomain i powrócił do wywołującego AppDomain rzucać

Minusem jest to, że Twój program będzie musiał podwyższone wystarczające uprawnienia do utworzenia AppDomain.

Oto przykładowy program, który pokazuje, jak to działa (myślę, że możesz go skopiować i wkleić, jeśli podasz poprawne using s). Stworzyłem także this gist, jeśli jesteś zainteresowany.

public class Program 
{ 
    public static void Main() 
    { 
     try 
     { 
      var timebox = new Timebox(TimeSpan.FromSeconds(1)); 
      timebox.Execute(() => 
      { 
       // do your thing 
       for (var i = 0; i < 1000; i++) 
       { 
        Console.WriteLine(i); 
       } 
      }); 

      Console.WriteLine("Didn't Time Out"); 
     } 
     catch (TimeoutException e) 
     { 
      Console.WriteLine("Timed Out"); 
      // handle it 
     } 
     catch(Exception e) 
     { 
      Console.WriteLine("Another exception was thrown in your timeboxed function"); 
      // handle it 
     } 
     Console.WriteLine("Program Finished"); 
     Console.ReadLine(); 
    } 
} 

public class Timebox 
{ 
    private readonly TimeSpan _ts; 

    public Timebox(TimeSpan ts) 
    { 
     _ts = ts; 
    } 

    public void Execute(Action func) 
    { 
     AppDomain childDomain = null; 
     try 
     { 
      // Construct and initialize settings for a second AppDomain. Perhaps some of 
      // this is unnecessary but perhaps not. 
      var domainSetup = new AppDomainSetup() 
      { 
       ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase, 
       ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile, 
       ApplicationName = AppDomain.CurrentDomain.SetupInformation.ApplicationName, 
       LoaderOptimization = LoaderOptimization.MultiDomainHost 
      }; 

      // Create the child AppDomain 
      childDomain = AppDomain.CreateDomain("Timebox Domain", null, domainSetup); 

      // Create an instance of the timebox runtime child AppDomain 
      var timeboxRuntime = (ITimeboxRuntime)childDomain.CreateInstanceAndUnwrap(
       typeof(TimeboxRuntime).Assembly.FullName, typeof(TimeboxRuntime).FullName); 

      // Start the runtime, by passing it the function we're timboxing 
      Exception ex = null; 
      var timeoutOccurred = true; 
      var task = new Task(() => 
      { 
       ex = timeboxRuntime.Run(func); 
       timeoutOccurred = false; 
      }); 

      // start task, and wait for the alloted timespan. If the method doesn't finish 
      // by then, then we kill the childDomain and throw a TimeoutException 
      task.Start(); 
      task.Wait(_ts); 

      // if the timeout occurred then we throw the exception for the caller to handle. 
      if(timeoutOccurred) 
      { 
       throw new TimeoutException("The child domain timed out"); 
      } 

      // If no timeout occurred, then throw whatever exception was thrown 
      // by our child AppDomain, so that calling code "sees" the exception 
      // thrown by the code that it passes in. 
      if(ex != null) 
      { 
       throw ex; 
      } 
     } 
     finally 
     { 
      // kill the child domain whether or not the function has completed 
      if(childDomain != null) AppDomain.Unload(childDomain); 
     } 
    } 

    // don't strictly need this, but I prefer having an interface point to the proxy 
    private interface ITimeboxRuntime 
    { 
     Exception Run(Action action); 
    } 

    // Need to derive from MarshalByRefObject... proxy is returned across AppDomain boundary. 
    private class TimeboxRuntime : MarshalByRefObject, ITimeboxRuntime 
    { 
     public Exception Run(Action action) 
     { 
      try 
      { 
       // Nike: just do it! 
       action(); 
      } 
      catch(Exception e) 
      { 
       // return the exception to be thrown in the calling AppDomain 
       return e; 
      } 
      return null; 
     } 
    } 
} 

Edycja:

Powodem poszedł z AppDomain zamiast Thread sek lub tylko Task s, dlatego, że nie ma kuloodporne sposobem rozwiązania Thread S lub Task s arbitralne Kod [1] [2] [3]. AppDomain, dla twoich wymagań, wydawał mi się najlepszym podejściem do mnie.

+0

Chociaż nie zawsze można użyć podwyższonych uprawnień, jest to bardzo czyste rozwiązanie. Prawdopodobnie użyję tego w bitach i kawałkach. – Jerry

+0

Jeśli 'Thread.Abort()' jest czymś, czego wygodnie używasz, to działa dokładnie tak samo jak w/'AppDomain'. Zamiast "ex = timeboxRuntime.Run (func)" możesz przechwycić wątek (tak jak w opublikowanym rozwiązaniu) i wywołać 'func()' w zadaniu. Później, zamiast rozładowywać domenę, możesz przerwać wątek (jeśli nadal działa). To nie musi mieć podwyższonych uprawnień. ** Wciąż polecam używanie 'AppDomain' **, ponieważ zawsze przestrzegam [porady Erica Lipperta] (http://stackoverflow.com/questions/1559255/whats-wrong-with-using-thread-abort/1560567 # 1560567) :). –

2

Oto implementacja asynchroniczny z limity czasu:

... 
     private readonly semaphore = new SemaphoreSlim(1,1); 

    ... 
     // total time allowed here is 100ms 
     var tokenSource = new CancellationTokenSource(100); 
     try{ 
     await WorkMethod(parameters, tokenSource.Token); // work 
     } catch (OperationCancelledException ocx){ 
     // gracefully handle cancellations: 
     label.Text = "Operation timed out"; 
     } 
    ... 

    public async Task WorkMethod(object prm, CancellationToken ct){ 
     try{ 
     await sem.WaitAsync(ct); // equivalent to lock(object){...} 
     // synchronized work, 
     // call tokenSource.Token.ThrowIfCancellationRequested() or 
     // check tokenSource.IsCancellationRequested in long-running blocks 
     // and pass ct to other tasks, such as async HTTP or stream operations 
     } finally { 
     sem.Release(); 
     } 
    } 
NIE

że radzę go, ale mógłby zdać tokenSource zamiast jego Token do WorkMethod i okresowo zrobić tokenSource.CancelAfter(200) dodać więcej czasu, jeśli jesteś pewnie nie jesteś w miejscu, które może być zablokowane (oczekując na połączenie HTTP), ale myślę, że byłoby to ezoteryczne podejście do wielowątkowości.

Zamiast tego wątki powinny być tak szybkie, jak to możliwe (minimum IO), a jeden wątek może serializować zasoby (producent), podczas gdy inne przetwarzają kolejkę (konsumenci), jeśli trzeba postępować z wielowątkowością we/wy (np. Kompresja plików, pobieranie itp.) i całkowicie uniknąć możliwości zakleszczenia.

0

Bardzo podobał mi się wizualny pomysł użycia oświadczenia. Jednak, to nie jest opłacalne rozwiązanie. Czemu? Cóż, podtytuł (obiekt/wątek/timer w instrukcji using) nie może zakłócić głównego wątku i wprowadzić wyjątku, powodując zatrzymanie tego, co robił i przeskoczyć do najbliższego try/catch. To wszystko sprowadza się do. Im więcej siedziałem i pracowałem z tym, tym bardziej wyszło na jaw.

Krótko mówiąc, nie można tego zrobić tak, jak chciałem to zrobić.

Jednak podjąłem podejście Pietera i trochę zmanipulowałem. Wprowadza pewne problemy z czytelnością, ale starałem się złagodzić je za pomocą komentarzy i innych.

public void MyMethod(...) 
{ 

... 

    // Placeholder for thread to kill if the action times out. 
    Thread threadToKill = null; 
    Action wrappedAction =() => 
    { 
     // Take note of the action's thread. We may need to kill it later. 
     threadToKill = Thread.CurrentThread; 

     ... 
     /* DO STUFF HERE */ 
     ... 

    }; 

    // Now, execute the action. We'll deal with the action timeouts below. 
    IAsyncResult result = wrappedAction.BeginInvoke(null, null); 

    // Set the timeout to 10 minutes. 
    if (result.AsyncWaitHandle.WaitOne(10 * 60 * 1000)) 
    { 
     // Everything was successful. Just clean up the invoke and get out. 
     wrappedAction.EndInvoke(result); 
    } 
    else 
    { 
     // We have timed out. We need to abort the thread!! 
     // Don't let it continue to try to do work. Something may be stuck. 
     threadToKill.Abort(); 
     throw new TimeoutException("This code block timed out"); 
    } 

... 
} 

Ponieważ robię to w trzech lub czterech miejscach na główną część, jest to trudniejsze do przeczytania. Jednak działa całkiem dobrze.

+0

Nie ma potrzeby duplikowania całego tego kodu - możesz umieścić go w metodzie pomocniczej, która przyjmuje argument "Akcja" jako, i wywołaj akcję 'Action' z poziomu' wrappedAction'. Zauważ, że twój kod blokuje wątek wywołujący i że zaleca się pisanie kodu, który współpracuje z anulowaniem, zamiast "gwałtownie" przerwać wątek. Nowoczesne podejście polegałoby na użyciu 'Task' razem z' CancellationToken (Source) '(który, w wygodny sposób, może przyjąć jako argument czasowy). –

+1

@Jerry Jedna uwaga: "Thread.Abort()' jest kontrowersyjny. [Według Erica Lipperta] (http://stackoverflow.com/a/1560567/3960399), który jest dość autorytetem w C#, należy tego unikać za wszelką cenę. –

Powiązane problemy