Czy istnieje zamiennik na podstawie zadań dla System.Threading.Timer?

91

Jestem nowy w zadaniach .Net 4.0 i nie byłem w stanie znaleźć tego, co według mnie byłoby zastąpieniem opartym na zadaniach lub wdrożeniem timera, np. Okresowym zadaniem. Czy jest coś takiego?

Aktualizacja Wymyśliłem to, co uważam za rozwiązanie dla moich potrzeb, które polega na umieszczeniu funkcji „Timer” wewnątrz zadania z zadaniami podrzędnymi, które wykorzystują CancellationToken i zwraca zadanie, aby móc uczestniczyć w dalszych krokach zadania.

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);
}      
Jim
źródło
7
Zamiast używać mechanizmu Thread.Sleep, należy użyć timera wewnątrz zadania. Jest bardziej wydajna.
Yoann. B

Odpowiedzi:

85

To zależy od 4.5, ale to działa.

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

Oczywiście możesz dodać wersję ogólną, która również przyjmuje argumenty. Jest to w rzeczywistości podobne do innych sugerowanych podejść, ponieważ pod maską Task.Delay używa upływu czasu jako źródła zakończenia zadania.

Jeff
źródło
1
Właśnie przeszedłem na to podejście. Ale warunkowo dzwonię action()z powtórzeniem !cancelToken.IsCancellationRequested. Tak lepiej, prawda?
HappyNomad
3
Dzięki za to - używamy tego samego, ale przesunęliśmy opóźnienie na po akcji (ma to dla nas większy sens, ponieważ musimy natychmiast wywołać akcję, a następnie powtórzyć po x)
Michael Parker
2
Dzięki za to. Ale ten kod nie będzie działał „co X godzin”, będzie działał „co X godzin + czas actionwykonania”, prawda?
Alex
Poprawny. Potrzebowałbyś trochę matematyki, jeśli chcesz uwzględnić czas wykonania. Jednak może to być trudne, jeśli czas wykonania przekroczy twój okres, itp ...
Jeff
57

UPDATE Mam znakowanie odpowiedź poniżej jako „odpowiedź”, gdyż jest to dość stary już, że należy przy użyciu asynchronicznej / czekają wzór. Nie musisz już tego odrzucać. lol


Jak odpowiedziała Amy, nie ma implementacji okresowej / czasowej opartej na zadaniach. Jednak w oparciu o moją oryginalną AKTUALIZACJĘ przekształciliśmy to w coś całkiem użytecznego i przetestowanego w produkcji. Pomyślałem, że podzielę się:

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

Wynik:

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 . . .
Jim
źródło
1
Wygląda to na świetny kod, ale zastanawiam się, czy jest to konieczne teraz, gdy istnieją słowa kluczowe async / await. Jak wypada Twoje podejście do tego tutaj: stackoverflow.com/a/14297203/122781 ?
HappyNomad
1
@HappyNomad, wygląda na to, że klasa PeriodicTaskFactory mogłaby korzystać z funkcji async / await dla aplikacji przeznaczonych dla .Net 4.5, ale dla nas nie możemy jeszcze przejść na .Net 4.5. Ponadto PeriodicTaskFactory zapewnia dodatkowe mechanizmy kończenia „timera”, takie jak maksymalna liczba iteracji i maksymalny czas trwania, a także zapewnia sposób, aby każda iteracja mogła czekać w ostatniej iteracji. Ale będę się starał dostosować to do używania async / await, kiedy przejdziemy do .Net 4.5
Jim
4
+1 Używam teraz twojej klasy, dzięki. Aby jednak grał dobrze z wątkiem interfejsu użytkownika, muszę zadzwonić TaskScheduler.FromCurrentSynchronizationContext()przed ustawieniem mainAction. Następnie przekazuję wynikowy harmonogram do MainPeriodicTaskAction, aby utworzył subTaskplik.
HappyNomad
2
Nie jestem pewien, to dobry pomysł, aby zablokować wątek, gdy może on wykonać użyteczną pracę. "Thread.Sleep (delayInMilliseconds)", "periodResetEvent.Wait (intervalInMilliseconds, cancelToken)" ... Następnie używasz timera, czekasz sprzętowo, więc żadne wątki nie są wydawane. Ale w twoim rozwiązaniu wątki są wydawane na nic.
RollingStone
2
@rollingstone Zgadzam się. Myślę, że to rozwiązanie w dużej mierze przeczy celowi zachowania asynchronicznego. O wiele lepiej jest używać timera i nie marnować nici. To po prostu sprawia wrażenie asynchronicznego bez żadnych korzyści.
Jeff,
12

To nie jest dokładnie w System.Threading.Tasks, ale Observable.Timer(lub prostsze Observable.Interval) z biblioteki Reactive Extensions jest prawdopodobnie tym, czego szukasz.

mstone
źródło
1
Np. Observable.Interval (TimeSpan.FromSeconds (1)). Subscribe (v => Debug.WriteLine (v));
Martin Capodici
1
Fajnie, ale czy te konstrukty reaktywne są możliwe do zablokowania?
Shmil The Cat
9

Do tej pory używałem zadania LongRunning TPL do cyklicznej pracy w tle związanej z procesorem zamiast licznika czasu wątków, ponieważ:

  • zadanie OC obsługuje anulowanie
  • licznik czasu wątków może uruchomić inny wątek podczas zamykania programu, powodując możliwe problemy z usuniętymi zasobami
  • szansa na przekroczenie: licznik wątków może uruchomić kolejny wątek, podczas gdy poprzedni jest nadal przetwarzany z powodu nieoczekiwanej długiej pracy (wiem, można temu zapobiec, zatrzymując i ponownie uruchamiając licznik czasu)

Jednak rozwiązanie TPL zawsze żąda dedykowanego wątku, który nie jest konieczny podczas oczekiwania na następną akcję (czyli przez większość czasu). Chciałbym użyć proponowanego rozwiązania Jeffa do wykonywania cyklicznej pracy związanej z procesorem w tle, ponieważ potrzebuje wątku Threadpool tylko wtedy, gdy jest do zrobienia, co jest lepsze dla skalowalności (zwłaszcza gdy okres interwału jest duży).

Aby to osiągnąć, sugerowałbym 4 adaptacje:

  1. Dodaj ConfigureAwait(false)do, Task.Delay()aby wykonać doWorkakcję w wątku puli wątków, w przeciwnym razie doWorkzostanie wykonana na wątku wywołującym, co nie jest ideą równoległości
  2. Trzymaj się wzorca anulowania, zgłaszając wyjątek TaskCanceledException (nadal wymagany?)
  3. Przekaż CancellationToken, aby doWorkumożliwić anulowanie zadania
  4. Dodaj parametr typu obiekt, aby podać informacje o stanie zadania (np. Zadanie TPL)

O punkcie 2 nie jestem pewien, czy async await nadal wymaga TaskCanceledExecption, czy jest to tylko najlepsza praktyka?

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

Prosimy o komentarz do proponowanego rozwiązania ...

Aktualizacja 2016-8-30

Powyższe rozwiązanie nie wywołuje natychmiast, doWork()ale zaczyna się await Task.Delay().ConfigureAwait(false)od osiągnięcia przełączenia wątku dla doWork(). Poniższe rozwiązanie rozwiązuje ten problem, opakowując pierwsze doWork()wywołanie wTask.Run() i czekając na nie.

Poniżej znajduje się ulepszona async \ await zamiennik dla Threading.Timer który wykonuje anulowalną pracę cykliczną i jest skalowalny (w porównaniu z rozwiązaniem TPL), ponieważ nie zajmuje żadnego wątku podczas oczekiwania na następną akcję.

Zauważ, że w przeciwieństwie do timera, czas oczekiwania ( period) jest stały, a nie czas cyklu; czas cyklu to suma czasu oczekiwania, którego czas trwania doWork()może się zmieniać.

    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);
    }
Erik Stroeken
źródło
Użycie ConfigureAwait(false)spowoduje zaplanowanie kontynuacji metody w puli wątków, więc tak naprawdę nie rozwiązuje drugiego punktu dotyczącego czasomierza wątków. Myślę też, że nie taskStatejest to konieczne; Przechwytywanie zmiennych lambda jest bardziej elastyczne i bezpieczne dla typów.
Stephen Cleary
1
Co naprawdę chcę zrobić jest wymiana await Task.Delay()i doWork()tak doWork()natychmiast wykonać podczas uruchamiania. Ale bez jakiejś sztuczki doWork()wykonałby wątek wywołujący za pierwszym razem i zablokowałby go. Stephen, czy masz rozwiązanie tego problemu?
Erik Stroeken
1
Najłatwiej jest po prostu owinąć całość w plik Task.Run.
Stephen Cleary
Tak, ale wtedy mogę po prostu wrócić do rozwiązania TPL, którego używam teraz, które deklaruje wątek, o ile działa pętla, a zatem jest mniej skalowalne niż to rozwiązanie.
Erik Stroeken
1

Musiałem wyzwolić cykliczne zadania asynchroniczne z metody synchronicznej.

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

To jest adaptacja odpowiedzi Jeffa. Zostaje zmieniony, aby przyjąć a. Func<Task> Zapewnia również, że okres jest częstotliwością uruchamiania, odejmując czas wykonywania zadania od okresu dla następnego opóźnienia.

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}");
    }
}
chris31389
źródło
0

TaskTimerNapotkałem podobny problem i napisałem klasę, która zwraca serię zadań, które kończą się na zegarze: https://github.com/ikriv/tasktimer/ .

using (var timer = new TaskTimer(1000).Start())
{
    // Call DoStuff() every second
    foreach (var task in timer)
    {
        await task;
        DoStuff();
    }
}
Ivan Krivyakov
źródło
-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; }
}

Prosty...

nim
źródło