it-swarm.com.de

Gibt es einen Task-basierten Ersatz für System.Threading.Timer?

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

Update Ich habe eine Lösung für meine Bedürfnisse gefunden, die darin besteht, die "Timer" -Funktionalität in eine Task mit untergeordneten Tasks zu packen, wobei alle das CancellationToken nutzen und die Task zurückgeben an weiteren Task-Schritten 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);
}      
77
Jim

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 ist tatsächlich ähnlich zu anderen vorgeschlagenen Ansätzen, da Task.Delay unter der Haube einen Zeitgeberablauf als Quelle für den Taskabschluss verwendet.

69
Jeff

UPDATE Ich bin und markiert die Antwort unten als "Antwort", da diese nun alt genug ist, um das async/await-Muster verwenden zu können. Kein Grund mehr, dies abzustimmen. LOL


Wie Amy antwortete, gibt es keine Tasked-basierte periodische/Timer-Implementierung. Basierend auf meinem ursprünglichen UPDATE haben wir dies jedoch zu etwas ziemlich Nützlichem entwickelt und in der Produktion getestet. 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 . . .
56
Jim

Es ist nicht genau in System.Threading.Tasks, aber Observable.Timer (oder einfacher Observable.Interval ) aus der Reactive Extensions-Bibliothek ist wahrscheinlich das, was Sie suchen.

12
mstone

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

  • die TPL-Task unterstützt den Abbruch 
  • der Threading-Zeitgeber könnte einen anderen Thread starten, während das Programm heruntergefahren wird, was möglicherweise zu Problemen mit den bereitgestellten Ressourcen führt
  • chance für Überlauf: Der Threading-Timer könnte einen anderen Thread starten, während der vorherige aufgrund unerwartet langer Arbeit noch verarbeitet wird (ich weiß, er kann durch Stoppen und Neustarten des Timers verhindert werden)

Die TPL-Lösung beansprucht jedoch immer einen dedizierten Thread, der beim Warten auf die nächste Aktion nicht erforderlich ist (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 ein Threadpool-Thread erforderlich ist, wenn Arbeiten ausgeführt werden müssen.

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

  1. Fügen Sie ConfigureAwait(false) zu Task.Delay() hinzu, um die doWork-Aktion für einen Thread-Pool-Thread auszuführen. Andernfalls wird doWork für den aufrufenden Thread ausgeführt
  2. Halten Sie sich an das Stornierungsmuster, indem Sie eine TaskCanceledException auslösen (noch erforderlich?).
  3. Leiten Sie das CancellationToken an doWork weiter, damit die Aufgabe abgebrochen werden kann
  4. Fügen Sie einen Parameter vom Typ Objekt hinzu, um Informationen zum Status des Tasks bereitzustellen (wie bei einem TPL-Task)

Zu Punkt 2: Ich bin nicht sicher, ob async erwartet noch die TaskCanceledExecption erfordert, oder ist es nur die beste Praxis?

    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 ein ...

Update 2016-8-30

Die obige Lösung ruft nicht sofort doWork() auf, sondern beginnt mit await Task.Delay().ConfigureAwait(false), um den Threadwechsel für doWork() zu erhalten. Die Lösung unten beseitigt dieses Problem, indem der erste doWork() -Aufruf in einen Task.Run() eingeschlossen wird und darauf gewartet wird.

Nachfolgend ist der verbesserte async\await-Ersatz für Threading.Timer aufgeführt, der stornierbare zyklische Arbeit ausführt und (im Vergleich zur TPL-Lösung) skalierbar ist, da er beim Warten auf die nächste Aktion keinen Thread belegt. 

Beachten Sie, dass die Wartezeit (period) im Gegensatz zum Timer konstant ist und nicht die Zykluszeit. Die Zykluszeit ist die Summe der Wartezeit und der Dauer von doWork(), die 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);
    }
8
Erik Stroeken

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 einen Func<Task> aufzunehmen. Außerdem wird sichergestellt, dass der Zeitraum so oft ausgeführt wird, indem die Laufzeit des Tasks vom 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
chris31389

Ich bin auf ein ähnliches Problem gestoßen und habe eine TaskTimer-Klasse geschrieben, die eine Reihe von Aufgaben zurückgibt, die beim Timer abgeschlossen 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();
    }
}
0
Ivan Krivyakov