1
votes

Comment réduire la fréquence de gestion des événements déclenchés en continu

J'apprends les tâches et async / await en c #. Veuillez donc considérer la stupidité de ma question.

Il y a un événement DummyEvent dans une classe. Un gestionnaire d'événements DummyEventHandler est abonné à cet événement et il gère une grande quantité de tâches liées au processeur, qui ne sont en fait pas nécessaires pour être utilisées si fréquemment.

Pour cette raison, si DummyEvent est déclenché en continu, je souhaite que DummyEventHandler réponde soit à une fréquence réduite, soit à la fin de cette continuité.

Donc, mon idée est d'extraire la tâche volumineuse dans une tâche distincte et de la faire retarder de 500 millisecondes avant qu'elle ne se poursuive. Une fois le délai terminé, il vérifiera si la même tâche a été planifiée à nouveau (déclenchement d'événement continu) ou non et évitera le gros calcul s'il est vrai.

Voici ma mise en œuvre naïve de cette idée: p >

int ReducedCall = 0;
int TotalCallActual = 0;

protected void DummyEventHandler(object sender, bool arg)
{
    TotalCallActual++;
    LargeCPUBoundTask(); // there is a green underline here, but I think it's ok, or.. is it?
}

async Task LargeCPUBoundTask()
{
    ReducedCall = TotalCallActual;

    await Task.Delay(500);
    // if this task is called again in this time, TotalCallActual will increase

    if (ReducedCall == TotalCallActual)
    {
        // do all the large tasks
        ……

        ReducedCall = 0;
        TotalCallActual = 0;
    }
}

Mais le problème est que je n'obtiens pas ce que je veux. La ligne Task.Delay (500) n'attend pas réellement, ou, si elle attend, il y a quelque chose qui ne va pas parce que je suis stupéfiant.

Une meilleure idée, ou tout amélioration / correction?

Demandez des informations supplémentaires.

Merci


4 commentaires

Vous parlez de la ligne verte sous MyTask mais vous n'avez inclus aucune méthode appelée de cette façon dans votre code. Et exactement comment et quand sont TotalCallActual et ReducedCall augmentés / diminués? Et que diriez-vous de créer un code que nous pouvons réellement tester?


@PeterBons désolé pour la réponse tardive, j'ai mis à jour ma question, MyTask est en fait LargeCPUBoundTask , c'était une erreur en simplifiant la question. TotalCallActual est augmenté chaque fois que le DummyEventHandler est appelé, avec la ligne: TotalCallActual ++; et réduit dans le gestionnaire d'événements.


C'est bon. J'ai mis à jour ma réponse


Depuis la publication de ma réponse, j'ai créé un référentiel GitHub avec une solution qui comprend des tests et un exemple de console app. Il est également disponible via NuGet (recherchez CVV.EventReducer).


3 Réponses :


1
votes

J'utiliserais le gestionnaire d'événements timer au lieu de votre DummyEventHandler Ajustez simplement la fréquence en millisecondes de la minuterie et ce sera tout. Vous pouvez créer un minuteur via un code sans l'ajouter à un formulaire en tant que contrôle. Je pense que c'est dans les contrôles communs lib.

J'espère que cela vous aidera. Bonne chance.


0 commentaires

2
votes

Pour ce faire, vous pouvez utiliser les extensions réactives :

protected async void DummyEventHandler(object sender, bool arg)
{
    TotalCallActual++;
    await LargeCPUBoundTask(); // there is no more green underline here
}

L'opérateur Throttle comme codé ci-dessus permettra à une valeur (événement) de devenir vraie toutes les secondes.

Ainsi, dans l'exemple de code ci-dessus, le texte faire quelque chose ne sera imprimé qu'une seule fois (après une seconde) même pendant l'événement est déclenché plusieurs fois.

Modifier
À propos, la raison de la ligne verte est que votre tâche n'est pas attendue. Pour résoudre ce problème, modifiez le code en:

void Main()
{
    var generator = new EventGenerator();
    var observable = Observable.FromEventPattern<EventHandler<bool>, bool>(
                h => generator.MyEvent += h,
                h => generator.MyEvent -= h);

    observable
        .Throttle(TimeSpan.FromSeconds(1))
        .Subscribe(s =>
        {
            Console.WriteLine("doing something");
        });

    // simulate rapid firing event
    for(int i = 0; i <= 100; i++)
        generator.RaiseEvent(); 

    // when no longer interested, dispose the subscription  
    subscription.Dispose(); 
}

public class EventGenerator
{
    public event EventHandler<bool> MyEvent;

    public void RaiseEvent()
    {
        if (MyEvent != null)
        {
            MyEvent(this, false);
        }
    }
}

Malheureusement, cela ne résoudra toujours pas votre problème car un événement ne peut pas être attendu, donc si l'événement est à nouveau déclenché pendant LargeCPUBoundTask exécute toujours un autre appel à LargeCPUBoundTask sera effectué afin que le travail se chevauche si vous comprenez ce que je veux dire. En d'autres termes, c'est pourquoi votre code ne fonctionne pas.


0 commentaires

1
votes

J'ai passé un peu plus de temps à réfléchir à ce problème et l'hypothèse que j'ai faite avec ma première solution était que l'événement se déclenche continuellement, alors qu'il pourrait simplement se déclencher une partie du temps pendant un moment, puis s'arrêter dans le problème réel.

Dans des cas comme celui-ci, la tâche liée au processeur ne se produirait que lors du premier déclenchement d'événement, puis si les événements se terminent avant la fin de cette tâche liée au processeur, les événements restants ne seront pas traités. Mais vous ne voudriez pas les gérer tous, juste le "dernier" (pas nécessairement le dernier, juste un de plus pour s'occuper du "nettoyage").

Donc, j'ai a mis à jour ma réponse pour inclure le cas d'utilisation où il y a des cas d'utilisation fréquents mais intermittents (c'est-à-dire des rafales d'événements puis silencieux), la bonne chose se produirait et une exécution finale de la tâche liée au processeur se produirait (mais toujours pas plus d'une tâche liée au processeur en cours d'exécution à une fois).

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        Sender s = new Sender();
        using (Listener l = new Listener(s))
        {
            s.BeginDemonstration();
        }
    }
}

class Sender
{
    const int ATTEMPTED_CALLS = 1000000;

    internal EventHandler frequencyReducedHandler;
    internal int actualCalls = 0;
    internal int ignoredCalls = 0;

    Task[] tasks = new Task[ATTEMPTED_CALLS];

    internal void BeginDemonstration()
    {
        int attemptedCalls;
        for (attemptedCalls = 0; attemptedCalls < ATTEMPTED_CALLS; attemptedCalls++)
        {
            tasks[attemptedCalls] = Task.Run(() => frequencyReducedHandler.Invoke(this, EventArgs.Empty));
            //frequencyReducedHandler?.BeginInvoke(this, EventArgs.Empty, null, null);
        }
        if (tasks[0] != null)
        {
            Task.WaitAll(tasks, Timeout.Infinite);
        }
        Console.WriteLine($"Attempted: {attemptedCalls}\tActual: {actualCalls}\tIgnored: {ignoredCalls}");
        Console.ReadKey();
    }
}

class Listener : IDisposable
{
    enum State
    {
        Waiting,
        Running,
        Queued
    }

    private readonly AutoResetEvent m_SingleEntry = new AutoResetEvent(true);
    private readonly Sender m_Sender;

    private int m_CurrentState = (int)State.Waiting;

    internal Listener(Sender sender)
    {
        m_Sender = sender;
        m_Sender.frequencyReducedHandler += Handler;
    }

    private async void Handler(object sender, EventArgs args)
    {
        int state = Interlocked.Increment(ref m_CurrentState);
        try
        {
            if (state <= (int)State.Queued) // Previous state was WAITING or RUNNING
            {
                // Ensure only one run at a time
                m_SingleEntry.WaitOne();
                try
                {
                    // Only one thread at a time here so
                    // no need for Interlocked.Increment
                    m_Sender.actualCalls++;
                    // Execute CPU intensive task
                    await Task.Delay(500);
                }
                finally
                {
                    // Allow a waiting thread to proceed
                    m_SingleEntry.Set();
                }
            }
            else
            {
                Interlocked.Increment(ref m_Sender.ignoredCalls);
            }
        }
        finally
        {
            Interlocked.Decrement(ref m_CurrentState);
        }
    }

    public void Dispose()
    {
        m_SingleEntry?.Dispose();
    }
}


0 commentaires