Vergehen C # -Timer in einem separaten Thread?

97

Läuft ein System.Timers.Timer in einem anderen Thread als dem Thread ab, der ihn erstellt hat?

Nehmen wir an, ich habe eine Klasse mit einem Timer, der alle 5 Sekunden ausgelöst wird. Wenn der Timer ausgelöst wird, wird in der abgelaufenen Methode ein Objekt geändert. Nehmen wir an, das Ändern dieses Objekts dauert lange, beispielsweise 10 Sekunden. Ist es möglich, dass ich in diesem Szenario auf Thread-Kollisionen stoße?

user113164
quelle
Dies könnte zu Problemen führen. Beachten Sie, dass Threadpool-Threads im Allgemeinen nicht für Prozesse mit langer Laufzeit ausgelegt sind.
Greg D
Ich bin darauf gestoßen und habe mit einem Windows-Dienst getestet. Was für mich funktioniert hat, war, den Timer als erste Anweisung im OnTimer-Ereignis zu deaktivieren, meine Aufgaben auszuführen und am Ende den Timer zu aktivieren. Dies funktioniert in einer Produktionsumgebung seit einiger Zeit zuverlässig.
Steve

Antworten:

60

Für System.Timers.Timer :

Siehe Brian Gideons Antwort unten

Für System.Threading.Timer :

In der MSDN-Dokumentation zu Timern heißt es:

Die System.Threading.Timer-Klasse führt Rückrufe für einen ThreadPool-Thread durch und verwendet das Ereignismodell überhaupt nicht.

Der Timer läuft also tatsächlich auf einem anderen Thread ab.

Joren
quelle
21
Stimmt, aber das ist eine ganz andere Klasse. Das OP fragte nach der System.Timers.Timer-Klasse.
Brian Gideon
1
Oh, du hast Recht. msdn.microsoft.com/en-us/library/system.timers.timer.aspx sagt "Das abgelaufene Ereignis wird in einem ThreadPool-Thread ausgelöst." Gleiche Schlussfolgerung von dort, nehme ich an.
Joren
6
Nun ja, aber so einfach ist das nicht. Siehe meine Antwort.
Brian Gideon
192

Es hängt davon ab, ob. Der System.Timers.Timerhat zwei Betriebsarten.

Wenn SynchronizingObjecteine ISynchronizeInvokeInstanz festgelegt ist, wird das ElapsedEreignis auf dem Thread ausgeführt, auf dem sich das Synchronisierungsobjekt befindet. Normalerweise sind diese ISynchronizeInvokeInstanzen nichts anderes als alte Controlund FormInstanzen, mit denen wir alle vertraut sind. In diesem Fall wird das ElapsedEreignis im UI-Thread aufgerufen und verhält sich ähnlich wie das System.Windows.Forms.Timer. Ansonsten hängt es wirklich von der spezifischen ISynchronizeInvokeInstanz ab, die verwendet wurde.

Wenn SynchronizingObjectnull ist, wird das ElapsedEreignis in einem ThreadPoolThread aufgerufen und verhält sich ähnlich wie das System.Threading.Timer. Tatsächlich verwendet es einen System.Threading.TimerBlick hinter die Kulissen und führt den Marshalling-Vorgang aus, nachdem es bei Bedarf den Timer-Rückruf erhalten hat.

Brian Gideon
quelle
4
Wenn Sie möchten, dass der Timer-Rückruf in einem neuen Thread ausgeführt wird, sollten Sie ein System.Threading.Timeroder verwenden System.Timers.Timer?
CJ7
1
@ cj7: Entweder kann man das machen.
Brian Gideon
und wenn ich eine Liste komplexer Typen (Personen) habe und eine Zeit in jeder Person haben möchte? Ich brauche dies auf demselben Thread (alle Personen), denn wenn es die First-Person-Methode aufruft, muss die zweite Person warten, bis die erste das verstrichene Ereignis beendet. Kann ich das machen?
Leandro De Mello Fagundes
4
Jeder ... System.Timers.Timerhat zwei Betriebsarten. Es kann auf einem zufällig zugewiesenen Thread-Pool-Thread oder auf einem beliebigen Thread ausgeführt werden, auf dem sich die ISynchronizeInvokeInstanz befindet. Ich weiß nicht, wie ich das klarer machen soll. System.Threading.Timerhat wenig (wenn überhaupt) mit der ursprünglichen Frage zu tun.
Brian Gideon
@LeandroDeMelloFagundes Kannst du das nicht verwenden lock?
Ozkan
24

Jedes verstrichene Ereignis wird im selben Thread ausgelöst, es sei denn, ein vorheriges verstrichenes Ereignis wird noch ausgeführt.

So erledigt es die Kollision für Sie

Versuchen Sie dies in eine Konsole zu setzen

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

Sie werden so etwas bekommen

10
6
12
6
12

Dabei ist 10 der aufrufende Thread und 6 und 12 werden aus dem abgelaufenen bg-Ereignis ausgelöst. Wenn Sie den Thread.Sleep (2000) entfernen; Sie werden so etwas bekommen

10
6
6
6
6

Da gibt es keine Kollisionen.

Aber das lässt dich immer noch mit einem Problem zurück. Wenn Sie das Ereignis alle 5 Sekunden auslösen und die Bearbeitung 10 Sekunden dauert, benötigen Sie eine Sperre, um einige Änderungen zu überspringen.

Simon
quelle
8
Durch Hinzufügen eines timer.Stop()am Anfang der Methode "Verstrichenes Ereignis" und eines timer.Start()am Ende der Methode "Verstrichenes Ereignis" wird verhindert, dass das Ereignis "Verstrichen" kollidiert.
Metro Schlumpf
4
Sie müssen nicht timer.Stop () setzen, sondern nur timer definieren.AutoReset = false; Dann machen Sie timer.Start (), nachdem Sie das Ereignis verarbeitet haben. Ich denke, dies ist ein besserer Weg, um Kollisionen zu vermeiden.
João Antunes
17

Für System.Timers.Timer in einem separaten Thread, wenn SynchronizingObject nicht festgelegt ist.

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

Ausgabe, die Sie sehen würden, wenn DummyTimer im Intervall von 5 Sekunden ausgelöst würde:

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

Wie zu sehen ist, wird OnDummyTimerFired im Workers-Thread ausgeführt.

Nein, weitere Komplikationen - Wenn Sie das Intervall auf 10 ms reduzieren,

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

Dies liegt daran, dass .NET einen neuen Thread für diesen Job erstellen würde, wenn die vorherige Ausführung von OnDummyTimerFired beim nächsten Häkchen nicht erfolgt.

Erschwerend kommt hinzu: "Die System.Timers.Timer-Klasse bietet eine einfache Möglichkeit, dieses Dilemma zu lösen. Sie macht eine öffentliche SynchronizingObject-Eigenschaft verfügbar. Durch Festlegen dieser Eigenschaft für eine Instanz eines Windows Form (oder eines Steuerelements in einem Windows Form) wird dies sichergestellt dass der Code in Ihrem Elapsed-Ereignishandler auf demselben Thread ausgeführt wird, auf dem das SynchronizingObject instanziiert wurde. "

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

Swab.Jat
quelle
Das ist ein gutes Beispiel. Ich wusste es bis jetzt nicht und ich muss hier und da ein paar Schlösser setzen. Perfekt.
Olaru Mircea
Und was ist, wenn der Timer das Ereignis "Abgelaufen" im Hauptthread einer Konsolenanwendung auslösen soll?
Jacktric
13

Wenn das abgelaufene Ereignis länger als das Intervall dauert, wird ein weiterer Thread erstellt, um das abgelaufene Ereignis auszulösen. Dafür gibt es jedoch eine Problemumgehung

static void timer_Elapsed(object sender, ElapsedEventArgs e)    
{     
   try
   {
      timer.Stop(); 
      Thread.Sleep(2000);        
      Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);    
   }
   finally
   {
     timer.Start();
   }
}
Rajan
quelle
+1 Einfache und saubere Antwort, aber das wird nicht immer funktionieren. Vielmehr sollte man die Eigenschaft lock oder SynchronizingObject des Timers verwenden.
RollerCosta