System.Threading.Timer w C # wydaje się nie działać. Działa bardzo szybko co 3 sekundy

112

Mam obiekt zegarowy. Chcę, żeby było uruchamiane co minutę. W szczególności powinien uruchamiać OnCallBackmetodę i stać się nieaktywny, gdy OnCallBackmetoda jest uruchomiona. Po OnCallBackzakończeniu metody (a OnCallBack) restartuje licznik czasu.

Oto, co mam teraz:

private static Timer timer;

private static void Main()
{
    timer = new Timer(_ => OnCallBack(), null, 0, 1000 * 10); //every 10 seconds
    Console.ReadLine();
}

private static void OnCallBack()
{
    timer.Change(Timeout.Infinite, Timeout.Infinite); //stops the timer
    Thread.Sleep(3000); //doing some long operation
    timer.Change(0, 1000 * 10);  //restarts the timer
}

Jednak wydaje się, że nie działa. Działa bardzo szybko co 3 sekundy. Nawet jeśli podniesiesz kropkę (1000 * 10). Wygląda na to, że przymyka oko1000 * 10

Co zrobiłem źle?

Alan Coromano
źródło
12
From Timer.Change: "Jeśli dueTime ma wartość zero (0), metoda wywołania zwrotnego jest wywoływana natychmiast.". Wygląda na to, że to dla mnie zero.
Damien_The_Unbeliever
2
Tak, ale co z tego? jest też okres.
Alan Coromano
10
A co, jeśli jest też okres? Zacytowane zdanie nie zawiera zastrzeżeń co do wartości okresu. Mówi tylko, że „jeśli ta wartość jest równa zero, natychmiast wywołam wywołanie zwrotne”.
Damien_The_Unbeliever
3
Co ciekawe, jeśli ustawisz zarówno dueTime, jak i period na 0, licznik czasu będzie działał co sekundę i rozpocznie się natychmiast.
Kelvin,

Odpowiedzi:

230

To nie jest prawidłowe użycie pliku System.Threading.Timer. Podczas tworzenia instancji Timera prawie zawsze należy wykonać następujące czynności:

_timer = new Timer( Callback, null, TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite );

To poinstruuje timer, aby tykał tylko raz po upływie interwału. Następnie w funkcji oddzwaniania zmieniasz licznik czasu po zakończeniu pracy, a nie wcześniej. Przykład:

private void Callback( Object state )
{
    // Long running operation
   _timer.Change( TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite );
}

Dlatego nie ma potrzeby stosowania mechanizmów blokujących, ponieważ nie ma współbieżności. Timer uruchomi następne wywołanie zwrotne po upływie następnego interwału + czas długotrwałej operacji.

Jeśli chcesz uruchomić stoper z dokładnością do N milisekund, proponuję zmierzyć czas długotrwałej operacji za pomocą Stopwatch, a następnie odpowiednio wywołać metodę Change:

private void Callback( Object state )
{
   Stopwatch watch = new Stopwatch();

   watch.Start();
   // Long running operation

   _timer.Change( Math.Max( 0, TIME_INTERVAL_IN_MILLISECONDS - watch.ElapsedMilliseconds ), Timeout.Infinite );
}

I zdecydowanie zachęcić ktoś robi .NET i wykorzystuje CLR, który nie czytać książki Jeffreya Richtera - CLR via C # , aby przeczytać to jak najszybciej. Liczniki czasu i pule wątków są tam szczegółowo wyjaśnione.

Ivan Zlatanov
źródło
6
Nie zgadzam się z tym private void Callback( Object state ) { // Long running operation _timer.Change( TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite ); }. Callbackmoże zostać wywołana ponownie przed zakończeniem operacji.
Alan Coromano
2
Chodziło mi o to, że Long running operationmoże to zająć znacznie więcej czasu TIME_INTERVAL_IN_MILLISECONDS. Co by się wtedy stało?
Alan Coromano
32
Oddzwonienie nie zostanie ponownie wywołane, o to chodzi. Dlatego jako drugi parametr przekazujemy Timeout.Infinite. Zasadniczo oznacza to, że nie zaznaczaj ponownie licznika czasu. Następnie zmień harmonogram, aby zaznaczyć po zakończeniu operacji.
Ivan Zlatanov
Początkujący w wątkach tutaj - czy myślisz, że jest to możliwe ThreadPool, jeśli zdasz stoper? Myślę o scenariuszu, w którym nowy wątek jest odradzany, aby wykonać zadanie w określonych odstępach czasu - a po ukończeniu jest przenoszony do puli wątków.
jedd.ahyoung
2
System.Threading.Timer to licznik czasu puli wątków, który wykonuje wywołania zwrotne w puli wątków, a nie w dedykowanym wątku. Gdy czasomierz zakończy procedurę wywołania zwrotnego, wątek, który wykonał wywołanie zwrotne, wraca do puli.
Ivan Zlatanov
14

Nie ma potrzeby zatrzymywania timera, zobacz fajne rozwiązanie z tego postu :

„Możesz pozwolić zegarowi na kontynuowanie wywoływania metody wywołania zwrotnego, ale opakuj kod, który nie jest ponownie wprowadzany, w Monitor.TryEnter / Exit. W takim przypadku nie ma potrzeby zatrzymywania / restartowania licznika czasu; nakładające się wywołania nie uzyskają blokady i nie powrócą natychmiast.”

private void CreatorLoop(object state) 
 {
   if (Monitor.TryEnter(lockObject))
   {
     try
     {
       // Work here
     }
     finally
     {
       Monitor.Exit(lockObject);
     }
   }
 }
Ivan Leonenko
źródło
to nie w moim przypadku. Muszę dokładnie zatrzymać stoper.
Alan Coromano
Czy próbujesz uniemożliwić nawiązanie połączenia zwrotnego więcej niż jeden raz? Jeśli nie, co próbujesz osiągnąć?
Ivan Leonenko
1. Uniemożliwienie wchodzenia do oddzwaniania więcej niż jeden raz. 2. Zapobiegaj wykonywaniu zbyt wielu razy.
Alan Coromano
Dokładnie to robi. # 2 nie jest zbyt dużym narzutem, o ile zwraca zaraz po instrukcji if, jeśli obiekt jest zablokowany, szczególnie jeśli masz tak duży interwał.
Ivan Leonenko
1
Nie gwarantuje to, że kod zostanie wywołany nie wcześniej niż <interval> po ostatnim wykonaniu (nowy takt timera może zostać uruchomiony w mikrosekundę po zwolnieniu blokady przez poprzedni tik). Zależy to od tego, czy jest to wymóg ścisły, czy nie (nie do końca wynika z opisu problemu).
Marco Mp
9

Czy używanie jest System.Threading.Timerobowiązkowe?

Jeśli nie, System.Timers.Timerma przydatne Start()i Stop()metody (i AutoResetwłaściwość, którą możesz ustawić na false, aby Stop()nie było potrzebne i po prostu wywołujesz Start()po wykonaniu).

Marco Mp
źródło
3
Tak, ale może to być prawdziwy wymóg lub po prostu tak się stało, że wybrano timer, ponieważ jest najczęściej używany. Niestety .NET ma mnóstwo obiektów czasowych, nakładających się na siebie w 90%, ale wciąż (czasami subtelnie) różnych. Oczywiście, jeśli jest to wymagane, to rozwiązanie w ogóle nie ma zastosowania.
Marco Mp
2
Zgodnie z dokumentacją : Klasa Systems.Timer jest dostępna tylko w .NET Framework. Nie jest uwzględniony w bibliotece .NET Standard i nie jest dostępny na innych platformach, takich jak .NET Core lub Universal Windows Platform. Na tych platformach, a także w celu zapewnienia przenośności na wszystkich platformach .NET, należy zamiast tego użyć klasy System.Threading.Timer.
NotAgain mówi Przywróć Monikę
3

Po prostu zrobiłbym:

private static Timer timer;
 private static void Main()
 {
   timer = new Timer(_ => OnCallBack(), null, 1000 * 10,Timeout.Infinite); //in 10 seconds
   Console.ReadLine();
 }

  private static void OnCallBack()
  {
    timer.Dispose();
    Thread.Sleep(3000); //doing some long operation
    timer = new Timer(_ => OnCallBack(), null, 1000 * 10,Timeout.Infinite); //in 10 seconds
  }

I zignoruj ​​parametr okresu, ponieważ próbujesz samodzielnie kontrolować okresy.


Oryginalny kod działa tak szybko, jak to możliwe, skoro trzymasz określając 0dla dueTimeparametru. Od Timer.Change:

Jeśli dueTime ma wartość zero (0), metoda wywołania zwrotnego jest wywoływana natychmiast.

Damien_The_Unbeliever
źródło
2
Czy konieczne jest pozbycie się timera? Dlaczego nie używasz Change()metody?
Alan Coromano
21
Utylizacja timera za każdym razem jest absolutnie niepotrzebna i niewłaściwa.
Ivan Zlatanov
0
 var span = TimeSpan.FromMinutes(2);
 var t = Task.Factory.StartNew(async delegate / () =>
   {
        this.SomeAsync();
        await Task.Delay(span, source.Token);
  }, source.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

source.Cancel(true/or not);

// or use ThreadPool(whit defaul options thread) like this
Task.Start(()=>{...}), source.Token)

jeśli lubisz użyć jakiejś pętli w środku ...

public async void RunForestRun(CancellationToken token)
{
  var t = await Task.Factory.StartNew(async delegate
   {
       while (true)
       {
           await Task.Delay(TimeSpan.FromSeconds(1), token)
                 .ContinueWith(task => { Console.WriteLine("End delay"); });
           this.PrintConsole(1);
        }
    }, token) // drop thread options to default values;
}

// And somewhere there
source.Cancel();
//or
token.ThrowIfCancellationRequested(); // try/ catch block requred.
Umka
źródło