Gibt es einen aufgabenbasierten Ersatz für System.Threading.Timer?


88

Ich bin neu in den Aufgaben von .Net 4.0 und konnte nicht finden, was ich für einen aufgabenbasierten Ersatz oder die Implementierung eines Timers hielt, z. B. eine periodische Aufgabe. Gibt es so etwas?

Update Ich habe eine Lösung für meine Anforderungen gefunden, die darin besteht, die "Timer" -Funktionalität in eine Aufgabe mit untergeordneten Aufgaben zu packen, die alle das CancellationToken nutzen, und die Aufgabe zurückzugeben, um an weiteren Aufgabenschritten teilnehmen zu können.

public static Task StartPeriodicTask(Action action, int intervalInMilliseconds, int delayInMilliseconds, CancellationToken cancelToken)
{ 
    Action wrapperAction = () =>
    {
        if (cancelToken.IsCancellationRequested) { return; }

        action();
    };

    Action mainAction = () =>
    {
        TaskCreationOptions attachedToParent = TaskCreationOptions.AttachedToParent;

        if (cancelToken.IsCancellationRequested) { return; }

        if (delayInMilliseconds > 0)
            Thread.Sleep(delayInMilliseconds);

        while (true)
        {
            if (cancelToken.IsCancellationRequested) { break; }

            Task.Factory.StartNew(wrapperAction, cancelToken, attachedToParent, TaskScheduler.Current);

            if (cancelToken.IsCancellationRequested || intervalInMilliseconds == Timeout.Infinite) { break; }

            Thread.Sleep(intervalInMilliseconds);
        }
    };

    return Task.Factory.StartNew(mainAction, cancelToken);
}      

7
Sie sollten einen Timer innerhalb der Task verwenden, anstatt den Thread.Sleep-Mechanismus zu verwenden. Es ist effizienter.
Yoann. B

Antworten:


84

Es hängt von 4.5 ab, aber das funktioniert.

public class PeriodicTask
{
    public static async Task Run(Action action, TimeSpan period, CancellationToken cancellationToken)
    {
        while(!cancellationToken.IsCancellationRequested)
        {
            await Task.Delay(period, cancellationToken);

            if (!cancellationToken.IsCancellationRequested)
                action();
        }
     }

     public static Task Run(Action action, TimeSpan period)
     { 
         return Run(action, period, CancellationToken.None);
     }
}

Natürlich können Sie eine generische Version hinzufügen, die auch Argumente akzeptiert. Dies ähnelt tatsächlich anderen vorgeschlagenen Ansätzen, da Task.Delay unter der Haube einen Timer-Ablauf als Quelle für den Abschluss der Aufgabe verwendet.


1
Ich bin gerade zu diesem Ansatz übergegangen. Aber ich rufe bedingt action()mit einer Wiederholung von an !cancelToken.IsCancellationRequested. Das ist besser, oder?
HappyNomad

3
Vielen Dank dafür - wir verwenden dasselbe, haben aber die Verzögerung bis nach der Aktion verschoben (es ist für uns sinnvoller, da wir die Aktion sofort aufrufen und nach x wiederholen müssen)
Michael Parker

1
Danke dafür. Aber dieser Code wird nicht "alle X Stunden" ausgeführt, sondern "alle X Stunden + actionAusführungszeit". Habe ich recht?
Alex

Richtig. Sie benötigen etwas Mathematik, wenn Sie die Ausführungszeit berücksichtigen möchten. Dies kann jedoch schwierig werden, wenn die Ausführungszeit Ihren Zeitraum überschreitet usw.
Jeff

57

UPDATE Ich markiere die Antwort unten als "Antwort", da diese jetzt alt genug ist, um das asynchrone / wartende Muster zu verwenden. Sie müssen dies nicht mehr ablehnen. LOL


Wie Amy antwortete, gibt es keine Tasked-basierte periodische / Timer-Implementierung. Basierend auf meinem ursprünglichen UPDATE haben wir dies jedoch zu etwas sehr Nützlichem entwickelt und die Produktion getestet. Ich dachte, ich würde teilen:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication7
{
    class Program
    {
        static void Main(string[] args)
        {
            Task perdiodicTask = PeriodicTaskFactory.Start(() =>
            {
                Console.WriteLine(DateTime.Now);
            }, intervalInMilliseconds: 2000, // fire every two seconds...
               maxIterations: 10);           // for a total of 10 iterations...

            perdiodicTask.ContinueWith(_ =>
            {
                Console.WriteLine("Finished!");
            }).Wait();
        }
    }

    /// <summary>
    /// Factory class to create a periodic Task to simulate a <see cref="System.Threading.Timer"/> using <see cref="Task">Tasks.</see>
    /// </summary>
    public static class PeriodicTaskFactory
    {
        /// <summary>
        /// Starts the periodic task.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds, i.e. how long it waits to kick off the timer.</param>
        /// <param name="duration">The duration.
        /// <example>If the duration is set to 10 seconds, the maximum time this task is allowed to run is 10 seconds.</example></param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create the task for executing the <see cref="Action"/>.</param>
        /// <returns>A <see cref="Task"/></returns>
        /// <remarks>
        /// Exceptions that occur in the <paramref name="action"/> need to be handled in the action itself. These exceptions will not be 
        /// bubbled up to the periodic task.
        /// </remarks>
        public static Task Start(Action action,
                                 int intervalInMilliseconds = Timeout.Infinite,
                                 int delayInMilliseconds = 0,
                                 int duration = Timeout.Infinite,
                                 int maxIterations = -1,
                                 bool synchronous = false,
                                 CancellationToken cancelToken = new CancellationToken(),
                                 TaskCreationOptions periodicTaskCreationOptions = TaskCreationOptions.None)
        {
            Stopwatch stopWatch = new Stopwatch();
            Action wrapperAction = () =>
            {
                CheckIfCancelled(cancelToken);
                action();
            };

            Action mainAction = () =>
            {
                MainPeriodicTaskAction(intervalInMilliseconds, delayInMilliseconds, duration, maxIterations, cancelToken, stopWatch, synchronous, wrapperAction, periodicTaskCreationOptions);
            };

            return Task.Factory.StartNew(mainAction, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }

        /// <summary>
        /// Mains the periodic task action.
        /// </summary>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds.</param>
        /// <param name="duration">The duration.</param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="stopWatch">The stop watch.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="wrapperAction">The wrapper action.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create a sub task for executing the <see cref="Action"/>.</param>
        private static void MainPeriodicTaskAction(int intervalInMilliseconds,
                                                   int delayInMilliseconds,
                                                   int duration,
                                                   int maxIterations,
                                                   CancellationToken cancelToken,
                                                   Stopwatch stopWatch,
                                                   bool synchronous,
                                                   Action wrapperAction,
                                                   TaskCreationOptions periodicTaskCreationOptions)
        {
            TaskCreationOptions subTaskCreationOptions = TaskCreationOptions.AttachedToParent | periodicTaskCreationOptions;

            CheckIfCancelled(cancelToken);

            if (delayInMilliseconds > 0)
            {
                Thread.Sleep(delayInMilliseconds);
            }

            if (maxIterations == 0) { return; }

            int iteration = 0;

            ////////////////////////////////////////////////////////////////////////////
            // using a ManualResetEventSlim as it is more efficient in small intervals.
            // In the case where longer intervals are used, it will automatically use 
            // a standard WaitHandle....
            // see http://msdn.microsoft.com/en-us/library/vstudio/5hbefs30(v=vs.100).aspx
            using (ManualResetEventSlim periodResetEvent = new ManualResetEventSlim(false))
            {
                ////////////////////////////////////////////////////////////
                // Main periodic logic. Basically loop through this block
                // executing the action
                while (true)
                {
                    CheckIfCancelled(cancelToken);

                    Task subTask = Task.Factory.StartNew(wrapperAction, cancelToken, subTaskCreationOptions, TaskScheduler.Current);

                    if (synchronous)
                    {
                        stopWatch.Start();
                        try
                        {
                            subTask.Wait(cancelToken);
                        }
                        catch { /* do not let an errant subtask to kill the periodic task...*/ }
                        stopWatch.Stop();
                    }

                    // use the same Timeout setting as the System.Threading.Timer, infinite timeout will execute only one iteration.
                    if (intervalInMilliseconds == Timeout.Infinite) { break; }

                    iteration++;

                    if (maxIterations > 0 && iteration >= maxIterations) { break; }

                    try
                    {
                        stopWatch.Start();
                        periodResetEvent.Wait(intervalInMilliseconds, cancelToken);
                        stopWatch.Stop();
                    }
                    finally
                    {
                        periodResetEvent.Reset();
                    }

                    CheckIfCancelled(cancelToken);

                    if (duration > 0 && stopWatch.ElapsedMilliseconds >= duration) { break; }
                }
            }
        }

        /// <summary>
        /// Checks if cancelled.
        /// </summary>
        /// <param name="cancelToken">The cancel token.</param>
        private static void CheckIfCancelled(CancellationToken cancellationToken)
        {
            if (cancellationToken == null)
                throw new ArgumentNullException("cancellationToken");

            cancellationToken.ThrowIfCancellationRequested();
        }
    }
}

Ausgabe:

2/18/2013 4:17:13 PM
2/18/2013 4:17:15 PM
2/18/2013 4:17:17 PM
2/18/2013 4:17:19 PM
2/18/2013 4:17:21 PM
2/18/2013 4:17:23 PM
2/18/2013 4:17:25 PM
2/18/2013 4:17:27 PM
2/18/2013 4:17:29 PM
2/18/2013 4:17:31 PM
Finished!
Press any key to continue . . .

1
Das sieht nach großartigem Code aus, aber ich frage mich, ob es jetzt notwendig ist, da es die Schlüsselwörter async / await gibt. Wie ist Ihr Ansatz im Vergleich zu dem hier: stackoverflow.com/a/14297203/122781 ?
HappyNomad

1
@HappyNomad, es sieht so aus, als könnte die PeriodicTaskFactory-Klasse den Vorteil async / await für Anwendungen nutzen, die auf .Net 4.5 abzielen, aber für uns können wir noch nicht zu .Net 4.5 wechseln. Außerdem bietet die PeriodicTaskFactory einige zusätzliche "Timer" -Abbruchmechanismen wie die maximale Anzahl von Iterationen und die maximale Dauer sowie eine Möglichkeit, um sicherzustellen, dass jede Iteration auf die letzte Iteration warten kann. Aber ich werde versuchen, dies anzupassen, um async / await zu verwenden, wenn wir zu .Net 4.5
Jim

4
+1 Ich benutze jetzt deine Klasse, danke. Damit es mit dem UI-Thread gut funktioniert, muss ich TaskScheduler.FromCurrentSynchronizationContext()vor dem Einstellen aufrufen mainAction. Ich übergebe dann den resultierenden Scheduler, MainPeriodicTaskActiondamit er das subTaskmit erstellt.
HappyNomad

2
Ich bin mir nicht sicher, ob dies eine gute Idee ist, einen Thread zu blockieren, wenn er nützliche Arbeit leisten kann. "Thread.Sleep (delayInMilliseconds)", "periodResetEvent.Wait (intervalInMilliseconds, cancelToken)" ... Dann verwenden Sie einen Timer, Sie warten in Hardware, damit keine Threads ausgegeben werden. Aber in Ihrer Lösung werden Threads für nichts ausgegeben.
RollingStone

2
@rollingstone Ich stimme zu. Ich denke, diese Lösung macht den Zweck eines asynchronen Verhaltens weitgehend zunichte. Es ist viel besser, einen Timer zu verwenden und den Faden nicht zu verschwenden. Dies gibt nur den Anschein von Async ohne einen der Vorteile.
Jeff


9

Bisher habe ich anstelle des Threading-Timers eine LongRunning-TPL-Task für zyklische CPU-gebundene Hintergrundarbeiten verwendet, weil:

  • Die TPL-Task unterstützt das Abbrechen
  • Der Threading-Timer könnte einen anderen Thread starten, während das Programm heruntergefahren wird, was zu möglichen Problemen mit den verfügbaren Ressourcen führen kann
  • Überlaufgefahr: Der Threading-Timer könnte aufgrund unerwartet langer Arbeit einen anderen Thread starten, während der vorherige noch verarbeitet wird (ich weiß, dies kann durch Anhalten und Neustarten des Timers verhindert werden.)

Die TPL-Lösung beansprucht jedoch immer einen dedizierten Thread, der nicht erforderlich ist, während auf die nächste Aktion gewartet wird (was meistens der Fall ist). Ich möchte die vorgeschlagene Lösung von Jeff verwenden, um CPU-gebundene zyklische Arbeit im Hintergrund auszuführen, da nur dann ein Threadpool-Thread benötigt wird, wenn Arbeit zu erledigen ist, die für die Skalierbarkeit besser ist (insbesondere wenn die Intervallperiode groß ist).

Um dies zu erreichen, würde ich 4 Anpassungen vorschlagen:

  1. Fügen Sie hinzu ConfigureAwait(false), Task.Delay()um die doWorkAktion ansonsten für einen Thread-Pool-Thread auszuführendoWork wird sie für den aufrufenden Thread ausgeführt, was nicht die Idee der Parallelität ist
  2. Halten Sie sich an das Stornierungsmuster, indem Sie eine TaskCanceledException auslösen (noch erforderlich?)
  3. Leiten Sie das CancellationToken an weiter doWork , damit es die Aufgabe abbrechen kann
  4. Fügen Sie einen Parameter vom Typ Objekt hinzu, um Informationen zum Aufgabenstatus bereitzustellen (z. B. eine TPL-Aufgabe).

Zu Punkt 2 Ich bin mir nicht sicher, ob für das asynchrone Warten noch die TaskCanceledExecption erforderlich ist, oder handelt es sich nur um eine bewährte Methode?

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }

Bitte geben Sie Ihre Kommentare zu der vorgeschlagenen Lösung ...

Update 30.08.2016

Die obige Lösung ruft nicht sofort auf, doWork()sondern beginnt mit await Task.Delay().ConfigureAwait(false)dem Erreichen des Thread-Wechsels für doWork(). Die folgende Lösung überwindet dieses Problem, indem der erste doWork()Anruf in a eingeschlossen Task.Run()und abgewartet wird.

Im Folgenden finden Sie die verbesserte asynchrone \ wait-Ersetzung Threading.Timer, die abbrechbare zyklische Arbeiten ausführt und skalierbar ist (im Vergleich zur TPL-Lösung), da sie beim Warten auf die nächste Aktion keinen Thread belegt.

Beachten Sie, dass im Gegensatz zum Timer die Wartezeit ( period) konstant ist und nicht die Zykluszeit. Die Zykluszeit ist die Summe der Wartezeit und deren Dauer doWork()variieren kann.

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        await Task.Run(() => doWork(taskState, cancellationToken), cancellationToken).ConfigureAwait(false);
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }

Mit ConfigureAwait(false)wird die Fortsetzung der Methode für den Thread-Pool geplant, sodass der zweite Punkt des Threading-Timers nicht wirklich gelöst wird. Ich denke auch nicht, dass taskStatees notwendig ist; Die Erfassung von Lambda-Variablen ist flexibler und typsicherer.
Stephen Cleary

1
Was ich wirklich tun möchte ist auszutauschen await Task.Delay()und doWork()so doWork()würde sofort während des Startvorgangs ausgeführt werden . Aber ohne einen Trick doWork()würde der aufrufende Thread das erste Mal ausgeführt und blockiert. Stephen, hast du eine Lösung für dieses Problem?
Erik Stroeken

1
Der einfachste Weg ist, das Ganze einfach in eine zu wickeln Task.Run.
Stephen Cleary

Ja, aber dann kann ich einfach zu der TPL-Lösung zurückkehren, die ich jetzt verwende und die einen Thread beansprucht, solange die Schleife ausgeführt wird und daher weniger skalierbar ist als diese Lösung.
Erik Stroeken

1

Ich musste die wiederkehrenden asynchronen Aufgaben von einer synchronen Methode auslösen.

public static class PeriodicTask
{
    public static async Task Run(
        Func<Task> action,
        TimeSpan period,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        while (!cancellationToken.IsCancellationRequested)
        {

            Stopwatch stopwatch = Stopwatch.StartNew();

            if (!cancellationToken.IsCancellationRequested)
                await action();

            stopwatch.Stop();

            await Task.Delay(period - stopwatch.Elapsed, cancellationToken);
        }
    }
}

Dies ist eine Adaption von Jeffs Antwort. Es wird geändert, um a aufzunehmen. Func<Task> Außerdem wird sichergestellt, dass der Zeitraum wie oft ausgeführt wird, indem die Laufzeit der Aufgabe von dem Zeitraum für die nächste Verzögerung abgezogen wird.

class Program
{
    static void Main(string[] args)
    {
        PeriodicTask
            .Run(GetSomething, TimeSpan.FromSeconds(3))
            .GetAwaiter()
            .GetResult();
    }

    static async Task GetSomething()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Console.WriteLine($"Hi {DateTime.UtcNow}");
    }
}

0

Ich bin auf ein ähnliches Problem TaskTimergestoßen und habe eine Klasse geschrieben, die eine Reihe von Aufgaben zurückgibt, die auf dem Timer ausgeführt werden: https://github.com/ikriv/tasktimer/ .

using (var timer = new TaskTimer(1000).Start())
{
    // Call DoStuff() every second
    foreach (var task in timer)
    {
        await task;
        DoStuff();
    }
}

-1
static class Helper
{
    public async static Task ExecuteInterval(Action execute, int millisecond, IWorker worker)
    {
        while (worker.Worked)
        {
            execute();

            await Task.Delay(millisecond);
        }
    }
}


interface IWorker
{
    bool Worked { get; }
}

Einfach...

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.