Der richtige Weg, um eine nie endende Aufgabe zu implementieren. (Timer gegen Aufgabe)

92

Daher muss meine App eine Aktion fast ununterbrochen ausführen (mit einer Pause von ungefähr 10 Sekunden zwischen jedem Lauf), solange die App ausgeführt wird oder eine Stornierung angefordert wird. Die zu erledigende Arbeit kann bis zu 30 Sekunden dauern.

Ist es besser, einen System.Timers.Timer zu verwenden und AutoReset zu verwenden, um sicherzustellen, dass die Aktion nicht ausgeführt wird, bevor das vorherige "Häkchen" abgeschlossen ist.

Oder sollte ich eine allgemeine Aufgabe im LongRunning-Modus mit einem Stornierungs-Token verwenden und eine reguläre Endlos-while-Schleife darin haben, die die Aktion aufruft, die die Arbeit mit einem 10-Sekunden-Thread ausführt. Zwischen den Aufrufen schlafen? Was das asynchrone / wartende Modell betrifft, bin ich mir nicht sicher, ob es hier angemessen wäre, da ich keine Rückgabewerte von der Arbeit habe.

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

oder verwenden Sie einfach einen einfachen Timer, während Sie die AutoReset-Eigenschaft verwenden, und rufen Sie .Stop () auf, um ihn abzubrechen?

Josh
quelle
Aufgabe scheint ein Overkill zu sein, wenn man bedenkt, was Sie erreichen wollen. en.wikipedia.org/wiki/KISS_principle . Stoppen Sie den Timer beim Start von OnTick (), überprüfen Sie einen Bool, um festzustellen, ob Sie etwas nicht tun sollten, arbeiten Sie und starten Sie den Timer neu, wenn Sie fertig sind.
Mike Trusov

Antworten:

94

Ich würde dafür TPL Dataflow verwenden (da Sie .NET 4.5 verwenden und es Taskintern verwendet). Sie können ganz einfach ein ActionBlock<TInput>Element erstellen, das Elemente an sich selbst sendet, nachdem die Aktion verarbeitet und eine angemessene Zeit gewartet wurde.

Erstellen Sie zunächst eine Fabrik, die Ihre unendliche Aufgabe erstellt:

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

Ich habe mich für ActionBlock<TInput>eine DateTimeOffsetStruktur entschieden . Sie müssen einen Typparameter übergeben, der auch einen nützlichen Status übergeben kann (Sie können die Art des Status ändern, wenn Sie möchten).

Beachten Sie außerdem, dass ActionBlock<TInput>standardmäßig jeweils nur ein Element verarbeitet wird, sodass garantiert ist, dass nur eine Aktion verarbeitet wird (dh, Sie müssen sich nicht mit der Wiedereintrittsrate befassen, wenn die PostErweiterungsmethode auf sich selbst zurückgerufen wird).

Ich habe die CancellationTokenStruktur auch sowohl an den Konstruktor des ActionBlock<TInput>als auch an den Task.DelayMethodenaufruf übergeben . Wenn der Vorgang abgebrochen wird, erfolgt die Stornierung bei der erstmöglichen Gelegenheit.

Von dort aus ist es ein einfaches Refactoring Ihres Codes, um die von implementierte ITargetBlock<DateTimeoffset>Schnittstelle zu speichern ActionBlock<TInput>(dies ist die übergeordnete Abstraktion, die Blöcke darstellt, die Verbraucher sind, und Sie möchten den Verbrauch durch einen Aufruf der PostErweiterungsmethode auslösen können ):

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

Ihre StartWorkMethode:

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

Und dann deine StopWorkMethode:

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

Warum sollten Sie hier TPL Dataflow verwenden? Einige Gründe:

Trennung von Bedenken

Die CreateNeverEndingTaskMethode ist jetzt eine Fabrik, die sozusagen Ihren "Service" erstellt. Sie steuern, wann es startet und stoppt, und es ist vollständig in sich geschlossen. Sie müssen die Statussteuerung des Timers nicht mit anderen Aspekten Ihres Codes verweben. Sie erstellen einfach den Block, starten ihn und stoppen ihn, wenn Sie fertig sind.

Effizientere Nutzung von Threads / Aufgaben / Ressourcen

Der Standardplaner für die Blöcke im TPL-Datenfluss ist der gleiche für a Task, bei dem es sich um den Thread-Pool handelt. Indem Sie das ActionBlock<TInput>zum Verarbeiten Ihrer Aktion sowie einen Aufruf von verwenden Task.Delay, erhalten Sie die Kontrolle über den Thread, den Sie verwendet haben, als Sie tatsächlich nichts getan haben. Zugegeben, dies führt tatsächlich zu einem gewissen Overhead, wenn Sie das neue TaskSystem erstellen, das die Fortsetzung verarbeitet. Dies sollte jedoch gering sein, da Sie dies nicht in einer engen Schleife verarbeiten (Sie warten zehn Sekunden zwischen den Aufrufen).

Wenn die DoWorkFunktion tatsächlich abwartbar gemacht werden kann (nämlich indem sie a zurückgibt Task), können Sie dies (möglicherweise) noch weiter optimieren, indem Sie die oben beschriebene Factory-Methode so anpassen, dass a Func<DateTimeOffset, CancellationToken, Task>anstelle von a verwendet wird Action<DateTimeOffset>, wie folgt:

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

Natürlich wäre es eine gute Praxis, die CancellationTokenMethode durchzuarbeiten (falls sie eine akzeptiert), was hier durchgeführt wird.

Das heißt, Sie hätten dann eine DoWorkAsyncMethode mit der folgenden Signatur:

Task DoWorkAsync(CancellationToken cancellationToken);

Sie müssten die StartWorkMethode ändern (nur geringfügig, und Sie bluten hier nicht die Trennung von Bedenken aus) , um die neue Signatur zu berücksichtigen, die an die CreateNeverEndingTaskMethode übergeben wird, wie folgt:

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);
}
casperOne
quelle
Hallo, ich versuche diese Implementierung, aber ich habe Probleme. Wenn mein DoWork kein Argument akzeptiert, task = CreateNeverEndingTask (now => DoWork (), wtoken.Token); gibt mir einen Build-Fehler (Typ Mismatch). Wenn mein DoWork dagegen einen DateTimeOffset-Parameter verwendet, gibt mir dieselbe Zeile einen anderen Erstellungsfehler, der besagt, dass keine Überladung für DoWork 0 Argumente akzeptiert. Würden Sie mir bitte helfen, dies herauszufinden?
Bovaz
1
Tatsächlich habe ich mein Problem gelöst, indem ich der Zeile, in der ich eine Aufgabe zuweisen, eine Umwandlung hinzugefügt und den Parameter an DoWork übergeben habe: task = (ActionBlock <DateTimeOffset>) CreateNeverEndingTask (now => DoWork (now), wtoken.Token);
Bovaz
Sie können auch den Typ der "ActionBlock <DateTimeOffset> -Aufgabe" ändern. zur ITargetBlock <DateTimeOffset> -Aufgabe;
XOR
1
Ich glaube, dies wird wahrscheinlich für immer Speicher zuweisen und letztendlich zu einem Überlauf führen.
Nate Gardner
@NateGardner In welchem ​​Teil?
CasperOne
75

Ich finde die neue aufgabenbasierte Oberfläche sehr einfach, um solche Dinge zu tun - sogar einfacher als die Verwendung der Timer-Klasse.

Es gibt einige kleine Anpassungen, die Sie an Ihrem Beispiel vornehmen können. Anstatt:

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

Du kannst das:

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

Auf diese Weise erfolgt die Stornierung sofort im Inneren Task.Delay, anstatt warten zu müssen, Thread.Sleepbis der Vorgang abgeschlossen ist.

Wenn Sie Task.Delayover verwenden, Thread.Sleepbedeutet dies auch, dass Sie keinen Thread binden, der für die Dauer des Schlafes nichts tut.

Wenn Sie in der Lage sind, können Sie auch DoWork()ein Stornierungs-Token akzeptieren, und die Stornierung reagiert viel schneller.

porges
quelle
1
Finden Sie heraus, welche Aufgabe Sie erhalten, wenn Sie das asynchrone Lambda als Parameter von Task.Factory.StartNew - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx verwenden, wenn Sie task.Wait ( ); Nachdem der Abbruch angefordert wurde, warten Sie auf eine falsche Aufgabe.
Lukas Pirkl
Ja, dies sollte eigentlich Task.Run sein, das die richtige Überlastung aufweist.
porges
Laut http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx wird anscheinend Task.Runder Thread-Pool verwendet, sodass Ihr Beispiel mit Task.Runstatt Task.Factory.StartNewmit TaskCreationOptions.LongRunningnicht genau dasselbe tut - Wenn ich die Aufgabe zur Verwendung der LongRunningOption benötigen würde, könnte ich sie dann nicht so verwenden, Task.Runwie Sie es gezeigt haben, oder fehlt mir etwas?
Jeff
@ Lumirris: Der Punkt von async / await besteht darin, zu vermeiden, dass ein Thread während der gesamten Ausführung gebunden wird (hier verwendet die Task während des Delay-Aufrufs keinen Thread). Die Verwendung LongRunningist also irgendwie nicht kompatibel mit dem Ziel, keine Threads zu binden. Wenn Sie sicherstellen möchten, dass ein eigener Thread ausgeführt wird, können Sie ihn verwenden. Hier starten Sie jedoch einen Thread, der die meiste Zeit im Ruhezustand ist. Was ist der Anwendungsfall?
porges
@Porges Punkt genommen. Mein Anwendungsfall wäre eine Aufgabe, bei der eine Endlosschleife ausgeführt wird, in der jede Iteration einen Teil der Arbeit erledigt und 2 Sekunden lang "entspannt" wird, bevor bei der nächsten Iteration ein weiterer Teil der Arbeit ausgeführt wird. Es läuft für immer, macht aber regelmäßig 2 Sekunden Pause. In meinem Kommentar ging es jedoch eher darum, ob Sie die LongRunningVerwendung der Task.RunSyntax angeben können . Aus der Dokumentation geht hervor, dass die Task.RunSyntax sauberer ist, solange Sie mit den verwendeten Standardeinstellungen zufrieden sind. Es scheint keine Überlastung zu geben, die ein TaskCreationOptionsArgument erfordert .
Jeff
4

Folgendes habe ich mir ausgedacht:

  • Erben Sie NeverEndingTaskdie ExecutionCoreMethode und überschreiben Sie sie mit der Arbeit, die Sie ausführen möchten.
  • Durch Ändern ExecutionLoopDelayMskönnen Sie die Zeit zwischen Schleifen anpassen, z. B. wenn Sie einen Backoff-Algorithmus verwenden möchten.
  • Start/Stop Bereitstellung einer synchronen Schnittstelle zum Starten / Stoppen der Aufgabe.
  • LongRunningbedeutet, dass Sie einen dedizierten Thread pro erhalten NeverEndingTask.
  • Diese Klasse weist im Gegensatz zur ActionBlockobigen basierten Lösung keinen Speicher in einer Schleife zu .
  • Der folgende Code ist eine Skizze, nicht unbedingt ein Produktionscode :)

::

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