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


Daher muss meine App fast ununterbrochen eine Aktion ausführen (mit einer Pause von etwa 10 Sekunden zwischen jedem Durchlauf), solange die App ausgeführt wird oder eine Stornierung angefordert wird. Die Arbeit, die es zu tun hat, hat die Möglichkeit, bis zu 30 Sekunden zu 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 wurde.

Oder sollte ich eine allgemeine Aufgabe im LongRunning-Modus mit einem Abbruch-Token verwenden und eine reguläre unendliche while-Schleife darin haben, die die Aktion aufruft und die Arbeit mit einem 10-Sekunden-Thread erledigt. Was das async / await-Modell betrifft, bin ich mir nicht sicher, ob es hier angebracht 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 Eigenschaft AutoReset verwenden, und rufen Sie .Stop () auf, um den Vorgang abzubrechen?


75
2017-12-04 03:12


Ursprung


Antworten:


Ich würde verwenden TPL Datenfluss dafür (da Sie .NET 4.5 verwenden und es verwendet Task im Inneren). Sie können einfach ein ActionBlock<TInput> der Artikel nach der Verarbeitung an sich selbst sendet und eine angemessene Zeit wartet.

Erstellen Sie zuerst eine Factory, die Ihre nie endende 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 das gewählt ActionBlock<TInput> etwas nehmen DateTimeOffset Struktur; Sie müssen einen Typparameter übergeben, und es kann auch einen nützlichen Zustand übergeben (Sie können die Art des Status ändern, wenn Sie möchten).

Beachten Sie auch, dass die ActionBlock<TInput> Standardmäßig nur Prozesse ein Sie können sicherstellen, dass nur eine Aktion verarbeitet wird (dh Sie müssen sich nicht damit befassen Reentranz wenn es anruft Post Erweiterungsmethode zurück auf sich selbst).

Ich habe auch die bestanden CancellationToken Struktur für den Konstruktor der ActionBlock<TInput> und zum Task.Delay Methode Anruf; Wenn der Prozess abgebrochen wird, wird die Stornierung bei der ersten möglichen Gelegenheit stattfinden.

Von dort ist es eine einfache Refactoring Ihres Codes zum Speichern der ITargetBlock<DateTimeoffset> Schnittstelle implementiert von ActionBlock<TInput> (Dies ist die Abstraktion auf höherer Ebene, die Blöcke darstellt, die Verbraucher sind, und Sie möchten den Verbrauch durch einen Aufruf an den Kunden auslösen können Post Erweiterungsmethode):

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

Ihre StartWork Methode:

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 dein StopWork Methode:

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 möchten Sie TPL Dataflow hier verwenden? Ein paar Gründe:

Trennung von Bedenken

Das CreateNeverEndingTask Methode ist jetzt eine Fabrik, die sozusagen Ihren "Service" schafft. Sie steuern, wann es startet und stoppt, und es ist vollständig eigenständig. Sie müssen die Statussteuerung des Timers nicht mit anderen Aspekten Ihres Codes verknüpfen. Sie erstellen einfach den Block, starten ihn und stoppen ihn, wenn Sie fertig sind.

Effizientere Verwendung von Threads / Aufgaben / Ressourcen

Der Standardplaner für die Blöcke im TPL-Datenfluss ist derselbe für a Task, das ist der Thread-Pool. Durch die Verwendung der ActionBlock<TInput> um Ihre Aktion zu verarbeiten, sowie einen Anruf an Task.Delay, du gibst die Kontrolle über den Thread, den du benutzt hast, wenn du gar nichts tust. Zugegeben, das führt zu etwas Overhead, wenn man das neue spawnt Taskdas wird die Fortsetzung verarbeiten, aber das sollte klein sein, wenn man bedenkt, dass Sie dies nicht in einer engen Schleife verarbeiten (Sie warten zehn Sekunden zwischen den Aufrufen).

Wenn die DoWork Funktion kann tatsächlich erwartet werden (nämlich, indem sie a zurückgibt Task), dann können Sie dies (möglicherweise) noch mehr optimieren, indem Sie die obige Factory - Methode anpassen, um ein Func<DateTimeOffset, CancellationToken, Task> statt einer Action<DateTimeOffset>, so:

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 Übung, das zu weben CancellationToken bis zu Ihrer Methode (wenn sie eine akzeptiert), die hier gemacht wird.

Das heißt, Sie hätten dann eine DoWorkAsync Methode mit folgender Signatur:

Task DoWorkAsync(CancellationToken cancellationToken);

Sie müssten sich ändern (nur leicht, und Sie sind nicht aussortieren Trennung von Bedenken hier) StartWork Methode zur Berücksichtigung der neuen Signatur, die an die CreateNeverEndingTask Methode, so:

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

88
2017-12-04 21:54



Ich finde die neue Task-basierte Schnittstelle sehr einfach für solche Dinge - sogar einfacher als die Timer-Klasse.

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

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 wird die Stornierung sofort ausgeführt, wenn innerhalb der Task.Delayanstatt auf das warten zu müssen Thread.Sleep beenden.

Auch mit Task.Delay Über Thread.Sleep bedeutet, dass Sie keinen Faden binden, der für die Dauer des Schlafes nichts tut.

Wenn Sie können, können Sie auch machen DoWork() Akzeptieren Sie ein Stornierungs-Token und die Stornierung wird viel reaktionsfähiger sein.


63
2017-12-04 03:33



Hier ist, was ich mir ausgedacht habe:

  • Geerbt von NeverEndingTask und überschreibe die ExecutionCore Methode mit der Arbeit, die Sie tun möchten.
  • Ändern ExecutionLoopDelayMs ermöglicht es Ihnen, die Zeit zwischen Schleifen z. wenn Sie einen Backoff-Algorithmus verwenden möchten.
  • Start/Stop Bereitstellen einer synchronen Schnittstelle zum Starten / Stoppen der Aufgabe.
  • LongRunning bedeutet, dass Sie einen eigenen Thread pro erhalten NeverEndingTask.
  • Diese Klasse reserviert Speicher nicht in einer Schleife im Gegensatz zu der ActionBlock basierte Lösung oben.
  • Der Code unten ist Skizze, nicht unbedingt 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();
    }
}

3
2018-06-07 00:29