Czy liczniki czasu C # upływają w osobnym wątku?

98

Czy System.Timers.Timer upływa w osobnym wątku niż wątek, który go utworzył?

Powiedzmy, że mam zajęcia ze stoperem, który uruchamia się co 5 sekund. Po uruchomieniu licznika czasu w metodzie elapsed jakiś obiekt jest modyfikowany. Powiedzmy, że modyfikacja tego obiektu zajmuje dużo czasu, np. 10 sekund. Czy to możliwe, że w tym scenariuszu napotkam kolizje wątków?

user113164
źródło
Może to prowadzić do problemów. Zwróć uwagę, że ogólnie wątki puli wątków nie są przeznaczone do długotrwałych procesów.
Greg D
Miałem do czynienia z tym, testując z usługą systemu Windows. U mnie zadziałało wyłączenie licznika czasu jako pierwsza instrukcja w zdarzeniu OnTimer, wykonanie moich zadań, a następnie włączenie licznika na końcu. Od jakiegoś czasu działa to niezawodnie w środowisku produkcyjnym.
Steve

Odpowiedzi:

60

Dla System.Timers.Timer :

Zobacz odpowiedź Briana Gideona poniżej

Dla System.Threading.Timer :

Dokumentacja MSDN dotycząca timerów stwierdza:

Klasa System.Threading.Timer wykonuje wywołania zwrotne w wątku ThreadPool i w ogóle nie używa modelu zdarzeń.

Więc rzeczywiście licznik czasu upływa w innym wątku.

Joren
źródło
21
To prawda, ale to zupełnie inna klasa. OP zapytał o klasę System.Timers.Timer.
Brian Gideon
1
Och, masz rację. msdn.microsoft.com/en-us/library/system.timers.timer.aspx mówi „Zdarzenie Elapsed jest zgłaszane w wątku ThreadPool”. Przypuszczam, że ten sam wniosek stamtąd.
Joren,
6
No tak, ale to nie jest takie proste. Zobacz moją odpowiedź.
Brian Gideon,
192

To zależy. System.Timers.TimerPosiada dwa tryby pracy.

Jeśli SynchronizingObjectjest ustawiona na ISynchronizeInvokeinstancję, Elapsedzdarzenie zostanie wykonane w wątku obsługującym obiekt synchronizujący. Zwykle te ISynchronizeInvokeprzypadki to nic innego jak zwykłe stare Controli Formprzypadki, które wszyscy znamy. W takim przypadku Elapsedzdarzenie jest wywoływane w wątku interfejsu użytkownika i zachowuje się podobnie do System.Windows.Forms.Timer. W przeciwnym razie tak naprawdę zależy to od konkretnego ISynchronizeInvokewystąpienia, które zostało użyte.

Jeśli SynchronizingObjectjest null, to Elapsedzdarzenie jest wywoływane w ThreadPoolwątku i zachowuje się podobnie do System.Threading.Timer. W rzeczywistości faktycznie używa System.Threading.Timerza kulisami i wykonuje operację kierowania po odebraniu wywołania zwrotnego timera, jeśli to konieczne.

Brian Gideon
źródło
4
Jeśli chcesz, aby wywołanie zwrotne licznika czasu było wykonywane w nowym wątku, czy powinieneś użyć System.Threading.Timerlub System.Timers.Timer?
CJ7
1
@ cj7: Każdy może to zrobić.
Brian Gideon
a jeśli mam listę typu złożonego (osoby) i chcę spędzić czas w każdej osobie? Potrzebuję tego, aby działał w tym samym wątku (wszystkie osoby), ponieważ jeśli wywołuje metodę pierwszej osoby, druga osoba musi poczekać, aż pierwsza zakończy zdarzenie, które upłynęło. Czy mogę to zrobić?
Leandro De Mello Fagundes
4
Każdy ... System.Timers.Timerma dwa tryby działania. Może działać w losowo przypisanym wątku puli wątków LUB może działać w dowolnym wątku, w którym znajduje się ISynchronizeInvokeinstancja. Nie wiem, jak to wyjaśnić. System.Threading.Timerma niewiele (jeśli cokolwiek) wspólnego z pierwotnym pytaniem.
Brian Gideon
@LeandroDeMelloFagundes Nie możesz tego użyć lock?
Ozkan
24

Każde zdarzenie Elapsed zostanie uruchomione w tym samym wątku, chyba że poprzednie Elapsed nadal działa.

Więc obsługuje kolizję za Ciebie

spróbuj umieścić to w konsoli

static void Main(string[] args)
{
    Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
    var timer = new Timer(1000);
    timer.Elapsed += timer_Elapsed;
    timer.Start();
    Console.ReadLine();
}

static void timer_Elapsed(object sender, ElapsedEventArgs e)
{
    Thread.Sleep(2000);
    Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
}

dostaniesz coś takiego

10
6
12
6
12

gdzie 10 jest wątkiem wywołującym, a 6 i 12 odpalają ze zdarzenia minęło bg. Jeśli usuniesz Thread.Sleep (2000); dostaniesz coś takiego

10
6
6
6
6

Ponieważ nie ma kolizji.

Ale to wciąż pozostawia problem. jeśli uruchamiasz zdarzenie co 5 sekund, a edycja zajmuje 10 sekund, potrzebujesz trochę blokady, aby pominąć niektóre edycje.

Szymon
źródło
8
Dodanie timer.Stop()na początku metody zdarzenia Elapsed, a następnie timer.Start()na końcu metody zdarzenia Elapsed, zapobiegnie kolizji zdarzenia Elapsed.
Metro Smurf
4
Nie musisz umieszczać timera.Stop (), wystarczy zdefiniować timer.AutoReset = false; następnie po przetworzeniu zdarzenia wykonujesz timer.Start (). Myślę, że to lepszy sposób na uniknięcie kolizji.
João Antunes
17

Dla System.Timers.Timer w oddzielnym wątku, jeśli SynchronizingObject nie jest ustawiony.

    static System.Timers.Timer DummyTimer = null;

    static void Main(string[] args)
    {
        try
        {

            Console.WriteLine("Main Thread Id: " + System.Threading.Thread.CurrentThread.ManagedThreadId);

            DummyTimer = new System.Timers.Timer(1000 * 5); // 5 sec interval
            DummyTimer.Enabled = true;
            DummyTimer.Elapsed += new System.Timers.ElapsedEventHandler(OnDummyTimerFired);
            DummyTimer.AutoReset = true;

            DummyTimer.Start();

            Console.WriteLine("Hit any key to exit");
            Console.ReadLine();
        }
        catch (Exception Ex)
        {
            Console.WriteLine(Ex.Message);
        }

        return;
    }

    static void OnDummyTimerFired(object Sender, System.Timers.ElapsedEventArgs e)
    {
        Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId);
        return;
    }

Na wyjściu zobaczysz, czy DummyTimer uruchomił się co 5 sekund:

Main Thread Id: 9
   12
   12
   12
   12
   12
   ... 

Tak więc, jak widać, OnDummyTimerFired jest wykonywany w wątku Workers.

Nie, dalsze komplikacje - jeśli zmniejszysz interwał do 10 ms,

Main Thread Id: 9
   11
   13
   12
   22
   17
   ... 

Dzieje się tak, ponieważ jeśli poprzednie wykonanie OnDummyTimerFired nie zostanie wykonane po uruchomieniu następnego ticka, platforma .NET utworzy nowy wątek do wykonania tego zadania.

Co jeszcze bardziej komplikuje sprawę , „Klasa System.Timers.Timer zapewnia łatwy sposób rozwiązania tego dylematu - ujawnia publiczną właściwość SynchronizingObject. Ustawienie tej właściwości na wystąpienie formularza systemu Windows (lub kontrolkę w formularzu systemu Windows) zapewni że kod w programie obsługi zdarzeń Elapsed działa w tym samym wątku, w którym utworzono wystąpienie obiektu SynchronizingObject. "

http://msdn.microsoft.com/en-us/magazine/cc164015.aspx#S2

Swab.Jat
źródło
To dobry przykład. Do tej pory nie wiedziałem i muszę tu i tam założyć kilka zamków. Idealny.
Olaru Mircea
A co, jeśli chcę, aby licznik czasu uruchamiał zdarzenie Elapsed w głównym wątku aplikacji konsolowej?
jacktric
13

Jeśli zdarzenie elapsed trwa dłużej niż interwał, utworzy kolejny wątek, aby zgłosić zdarzenie elapsed. Ale istnieje obejście tego problemu

static void timer_Elapsed(object sender, ElapsedEventArgs e)    
{     
   try
   {
      timer.Stop(); 
      Thread.Sleep(2000);        
      Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);    
   }
   finally
   {
     timer.Start();
   }
}
Rajan
źródło
+1 Prosta i jasna odpowiedź, ale nie zawsze będzie działać. Należy raczej użyć właściwości timera lock lub SynchronizingObject.
RollerCosta