2012-12-04 8 views
73

Moja aplikacja musi więc wykonać akcję niemal nieprzerwanie (z przerwą 10 sekund między kolejnymi uruchomieniami) tak długo, jak aplikacja jest uruchomiona lub wymagane jest anulowanie. Praca, którą musi wykonać, może zająć do 30 sekund.Właściwy sposób realizacji niekończącego się zadania. (Timery kontra Zadanie)

Czy lepiej jest użyć System.Timers.Timer i użyć AutoReset, aby upewnić się, że nie wykona akcji przed zakończeniem poprzedniego "tick".

Czy powinienem użyć ogólnego zadania w trybie LongRunning z tokenem anulowania i mieć regularną nieskończoną pętlę while wewnątrz wywołującą akcję wykonującą pracę z 10 sekundowym wątkiem. Śpisz między rozmowami? Jeśli chodzi o model async/await, nie jestem pewien, czy byłoby to odpowiednie, ponieważ nie mam żadnych wartości zwracanych z pracy.

CancellationTokenSource wtoken; 
Task task; 

void StopWork() 
{ 
    wtoken.Cancel(); 

    try 
    { 
     task.Wait(); 
    } catch(AggregateException) { } 
} 

void StartWork() 
{ 
    wtoken = new CancellationTokenSource(); 

    task = Task.Factory.StartNew(() => 
    { 
     while (true) 
     { 
      wtoken.Token.ThrowIfCancellationRequested(); 
      DoWork(); 
      Thread.Sleep(10000); 
     } 
    }, wtoken, TaskCreationOptions.LongRunning); 
} 

void DoWork() 
{ 
    // Some work that takes up to 30 seconds but isn't returning anything. 
} 

lub po prostu użyć prostego licznika czasu podczas korzystania z jego właściwości AutoReset i wywołać funkcję .Stop(), aby anulować?

+0

Zadanie wydaje się być przesadą biorąc pod uwagę to, co próbujesz osiągnąć. http://en.wikipedia.org/wiki/KISS_principle. Zatrzymaj stoper na początku OnTick(), sprawdź bool, aby zobaczyć, czy powinieneś robić cokolwiek na nie, pracuj, uruchom ponownie Timer, kiedy skończysz. –

Odpowiedz

87

Do tego używałbym TPL Dataflow (ponieważ używasz .NET 4.5 i używa wewnętrznie Task). Możesz łatwo utworzyć ActionBlock<TInput>, który umieszcza przedmioty po przetworzeniu działania i czeka na odpowiedni czas.

Najpierw należy utworzyć fabrykę, która stworzy swoją niekończącą zadanie:

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Action<DateTimeOffset> action, CancellationToken cancellationToken) 
{ 
    // Validate parameters. 
    if (action == null) throw new ArgumentNullException("action"); 

    // Declare the block variable, it needs to be captured. 
    ActionBlock<DateTimeOffset> block = null; 

    // Create the block, it will call itself, so 
    // you need to separate the declaration and 
    // the assignment. 
    // Async so you can wait easily when the 
    // delay comes. 
    block = new ActionBlock<DateTimeOffset>(async now => { 
     // Perform the action. 
     action(now); 

     // Wait. 
     await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken). 
      // Doing this here because synchronization context more than 
      // likely *doesn't* need to be captured for the continuation 
      // here. As a matter of fact, that would be downright 
      // dangerous. 
      ConfigureAwait(false); 

     // Post the action back to the block. 
     block.Post(DateTimeOffset.Now); 
    }, new ExecutionDataflowBlockOptions { 
     CancellationToken = cancellationToken 
    }); 

    // Return the block. 
    return block; 
} 

Wybrałam ActionBlock<TInput> wziąć DateTimeOffset structure; musisz przekazać parametr typu, a równie dobrze może przekazać jakiś użyteczny stan (możesz zmienić naturę stanu, jeśli chcesz).

Należy również pamiętać, że ActionBlock<TInput> przez procesów domyślnych tylko jeden pozycji na raz, więc masz gwarancję, że tylko jedna akcja zostanie przetworzony (znaczy, nie będzie mieć do czynienia z reentrancy kiedy to nazywa Post extension method z powrotem na siebie).

Podałem również CancellationToken structure zarówno konstruktorowi ActionBlock<TInput>, jak i Task.Delay method; jeśli proces zostanie anulowany, anulowanie nastąpi przy pierwszej możliwej okazji.

Stamtąd łatwa refaktoryzacja kodu do przechowywania ITargetBlock<DateTimeoffset> interface wdrożonego przez ActionBlock<TInput> (jest to abstrakcja wyższego poziomu reprezentująca bloki, które są konsumentami, i chcesz być w stanie wywołać konsumpcję poprzez wywołanie do Post metoda rozszerzenie):

CancellationTokenSource wtoken; 
ActionBlock<DateTimeOffset> task; 

Twój StartWork metoda:

void StartWork() 
{ 
    // Create the token source. 
    wtoken = new CancellationTokenSource(); 

    // Set the task. 
    task = CreateNeverEndingTask(now => DoWork(), wtoken.Token); 

    // Start the task. Post the time. 
    task.Post(DateTimeOffset.Now); 
} 

I wtedy metoda StopWork:

void StopWork() 
{ 
    // CancellationTokenSource implements IDisposable. 
    using (wtoken) 
    { 
     // Cancel. This will cancel the task. 
     wtoken.Cancel(); 
    } 

    // Set everything to null, since the references 
    // are on the class level and keeping them around 
    // is holding onto invalid state. 
    wtoken = null; 
    task = null; 
} 

Dlaczego chciałbyś używać tutaj TPL Dataflow? Kilka powodów:

Oddzielenie obawy

Sposób CreateNeverEndingTask jest fabryka, która tworzy swoją „usługa” że tak powiem. Kontrolujesz kiedy się uruchamia i zatrzymuje, i jest całkowicie samowystarczalny. Nie musisz przeplatać kontroli stanu timera z innymi aspektami twojego kodu. Po prostu utwórz blok, uruchom go i zatrzymaj, gdy skończysz.

Bardziej efektywne wykorzystanie wątków/zadań/zasobów

Domyślny planista dla bloków w OC przepływu danych jest taka sama dla Task, czyli puli wątków. Dzięki użyciu ActionBlock<TInput> do przetworzenia akcji, a także połączenia z numerem Task.Delay, uzyskujesz kontrolę nad wątkiem, którego używasz, kiedy nic nie robisz. To prawda, że ​​prowadzi to do pewnego obciążenia, gdy odradzamy nowe Task, które przetwarza kontynuację, ale to powinno być małe, biorąc pod uwagę, że nie przetwarzasz tego w ciasnej pętli (czekasz dziesięć sekund pomiędzy wywołaniami).

Jeśli funkcja DoWork rzeczywiście mogą być wykonane awaitable (mianowicie w tym, że zwraca Task), to można (ewentualnie) zoptymalizować ten jeszcze bardziej skomplikowany sposób fabryczną powyżej wziąć Func<DateTimeOffset, CancellationToken, Task> zamiast Action<DateTimeOffset>, jak tak:

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Func<DateTimeOffset, CancellationToken, Task> action, 
    CancellationToken cancellationToken) 
{ 
    // Validate parameters. 
    if (action == null) throw new ArgumentNullException("action"); 

    // Declare the block variable, it needs to be captured. 
    ActionBlock<DateTimeOffset> block = null; 

    // Create the block, it will call itself, so 
    // you need to separate the declaration and 
    // the assignment. 
    // Async so you can wait easily when the 
    // delay comes. 
    block = new ActionBlock<DateTimeOffset>(async now => { 
     // Perform the action. Wait on the result. 
     await action(now, cancellationToken). 
      // Doing this here because synchronization context more than 
      // likely *doesn't* need to be captured for the continuation 
      // here. As a matter of fact, that would be downright 
      // dangerous. 
      ConfigureAwait(false); 

     // Wait. 
     await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken). 
      // Same as above. 
      ConfigureAwait(false); 

     // Post the action back to the block. 
     block.Post(DateTimeOffset.Now); 
    }, new ExecutionDataflowBlockOptions { 
     CancellationToken = cancellationToken 
    }); 

    // Return the block. 
    return block; 
} 

oczywiście, byłoby dobrą praktyką jest splot CancellationToken przechodzenia na metody (jeśli przyjmuje jeden), który odbywa się tutaj.

Oznacza to byś wtedy mają DoWorkAsync metodę z następującym podpisem:

Task DoWorkAsync(CancellationToken cancellationToken); 

trzeba by zmienić (tylko nieznacznie, a ty nie jesteś krwawienie z separacji obawy tutaj) metodę StartWork w celu uwzględnienia nowego podpisu przekazany do metody CreateNeverEndingTask, tak:

void StartWork() 
{ 
    // Create the token source. 
    wtoken = new CancellationTokenSource(); 

    // Set the task. 
    task = CreateNeverEndingTask((now, ct) => DoWorkAsync(ct), wtoken.Token); 

    // Start the task. Post the time. 
    task.Post(DateTimeOffset.Now, wtoken.Token); 
} 
+0

Witam, próbuję tej implementacji, ale mam problemy. Jeśli mój DoWork nie przyjmuje argumentu, task = CreateNeverEndingTask (teraz => DoWork(), wtoken.Token); daje błąd kompilacji (niedopasowanie typu). Z drugiej strony, jeśli mój DoWork przyjmuje parametr DateTimeOffset, ta sama linia daje mi inny błąd kompilacji, informując mnie, że żadne przeciążenie dla DoWork nie przyjmuje 0 argumentów. Czy mógłbyś mi pomóc rozwiązać ten problem? – Bovaz

+1

W rzeczywistości, rozwiązałem problem przez dodanie rzutowania do wiersza, w którym przypisuję zadanie i przekazywanie parametru do DoWork: task = (ActionBlock ) CreateNeverEndingTask (teraz => DoWork (now), wtoken.Token); – Bovaz

+0

Można również zmienić typ zadania "ActionBlock "; do zadania ITargetBlock ; – XOR

61

Uważam, że nowy interfejs oparty na zadaniach jest bardzo prosty do robienia takich rzeczy - nawet łatwiejszy niż przy użyciu klasy Timer.

Istnieje kilka drobnych poprawek, które możesz wprowadzić do swojego przykładu. Zamiast:

task = Task.Factory.StartNew(() => 
{ 
    while (true) 
    { 
     wtoken.Token.ThrowIfCancellationRequested(); 
     DoWork(); 
     Thread.Sleep(10000); 
    } 
}, wtoken, TaskCreationOptions.LongRunning); 

Można to zrobić:

task = Task.Run(async() => // <- marked async 
{ 
    while (true) 
    { 
     DoWork(); 
     await Task.Delay(10000, wtoken.Token); // <- await with cancellation 
    } 
}, wtoken.Token); 

ten sposób anulowanie nastąpi natychmiast, jeśli wewnątrz Task.Delay, zamiast czekać na Thread.Sleep aby zakończyć.

Ponadto, używanie Task.Delay przez Thread.Sleep oznacza, że ​​nie wiążesz nici nic nie robiąc na czas snu.

Jeśli jesteś w stanie, możesz również ustawić DoWork() zaakceptować token anulowania, a anulowanie będzie znacznie bardziej elastyczne.

+0

Wyobraź sobie, jakie zadanie otrzymasz, jeśli użyjesz asynchronicznej lambdy jako parametru Task.Factory.StartNew - http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx do task.Wait(); po zażądaniu anulowania będziesz czekał na nieprawidłowe zadanie. –

+0

Tak, to powinno być teraz Task.Run teraz, które ma prawidłowe przeciążenie. – porges

+0

Według [http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx](http://blogs.msdn.com/b/pfxteam/archive/2011/10 /24/10229468.aspx) wygląda na to, że 'Task.Run' używa puli wątków, więc twój przykład używając' Task.Run' zamiast 'Task.Factory.StartNew' z' TaskCreationOptions.LongRunning' nie robi dokładnie tego to samo - gdybym potrzebował użyć opcji "LongRunning", czy nie byłbym w stanie użyć 'Task.Run', jak pokazałeś, czy też czegoś mi brakuje? – Jeff

3

Oto co wymyśliłem:

  • Należy dziedziczyć po NeverEndingTask i zastąpić metodę ExecutionCore zadaniami, które chcesz wykonać.
  • Zmiana ExecutionLoopDelayMs pozwala dostosować czas między pętlami, np. jeśli chcesz użyć algorytmu cofania.
  • Start/Stop zapewniają synchroniczny interfejs do uruchamiania/zatrzymywania zadania.
  • LongRunning oznacza, że ​​otrzymasz jeden wątek dedykowany na NeverEndingTask.
  • Ta klasa nie przydziela pamięci w pętli, w przeciwieństwie do powyższego rozwiązania opartego na ActionBlock.
  • Poniższy kod jest szkic, niekoniecznie kod produkcji :)

:

public abstract class NeverEndingTask 
{ 
    // Using a CTS allows NeverEndingTask to "cancel itself" 
    private readonly CancellationTokenSource _cts = new CancellationTokenSource(); 

    protected NeverEndingTask() 
    { 
     TheNeverEndingTask = new Task(
      () => 
      { 
       // Wait to see if we get cancelled... 
       while (!_cts.Token.WaitHandle.WaitOne(ExecutionLoopDelayMs)) 
       { 
        // Otherwise execute our code... 
        ExecutionCore(_cts.Token); 
       } 
       // If we were cancelled, use the idiomatic way to terminate task 
       _cts.Token.ThrowIfCancellationRequested(); 
      }, 
      _cts.Token, 
      TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning); 

     // Do not forget to observe faulted tasks - for NeverEndingTask faults are probably never desirable 
     TheNeverEndingTask.ContinueWith(x => 
     { 
      Trace.TraceError(x.Exception.InnerException.Message); 
      // Log/Fire Events etc. 
     }, TaskContinuationOptions.OnlyOnFaulted); 

    } 

    protected readonly int ExecutionLoopDelayMs = 0; 
    protected Task TheNeverEndingTask; 

    public void Start() 
    { 
     // Should throw if you try to start twice... 
     TheNeverEndingTask.Start(); 
    } 

    protected abstract void ExecutionCore(CancellationToken cancellationToken); 

    public void Stop() 
    { 
     // This code should be reentrant... 
     _cts.Cancel(); 
     TheNeverEndingTask.Wait(); 
    } 
}