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);
}
Odpowiedzi:
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.
źródło
action()
z powtórzeniem!cancelToken.IsCancellationRequested
. Tak lepiej, prawda?action
wykonania”, prawda?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 . . .
źródło
TaskScheduler.FromCurrentSynchronizationContext()
przed ustawieniemmainAction
. Następnie przekazuję wynikowy harmonogram doMainPeriodicTaskAction
, aby utworzyłsubTask
plik.To nie jest dokładnie w
System.Threading.Tasks
, aleObservable.Timer
(lub prostszeObservable.Interval
) z biblioteki Reactive Extensions jest prawdopodobnie tym, czego szukasz.źródło
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ż:
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:
ConfigureAwait(false)
do,Task.Delay()
aby wykonaćdoWork
akcję w wątku puli wątków, w przeciwnym raziedoWork
zostanie wykonana na wątku wywołującym, co nie jest ideą równoległościdoWork
umożliwić anulowanie zadaniaO 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 dladoWork()
. Poniższe rozwiązanie rozwiązuje ten problem, opakowując pierwszedoWork()
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 trwaniadoWork()
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); }
źródło
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 nietaskState
jest to konieczne; Przechwytywanie zmiennych lambda jest bardziej elastyczne i bezpieczne dla typów.await Task.Delay()
idoWork()
takdoWork()
natychmiast wykonać podczas uruchamiania. Ale bez jakiejś sztuczkidoWork()
wykonałby wątek wywołujący za pierwszym razem i zablokowałby go. Stephen, czy masz rozwiązanie tego problemu?Task.Run
.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}"); } }
źródło
TaskTimer
Napotkał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(); } }
źródło
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...
źródło