3
votes

.net core - Passage d'un nombre inconnu d'IProgress à la bibliothèque de classes

J'ai une application console qui utilise une bibliothèque de classes pour exécuter des tâches de longue durée. Ceci est une application de console principale .net et utilise l'hôte générique .net core. J'utilise également la bibliothèque ShellProgressBar pour afficher quelques barres de progression.

Mon service hébergé ressemble à ceci

public class MyService2 : IMyService
{
    private void List<IJob> _sequentialJobsToRun;
    private void List<IJob> _parallelJobsToRun;

    public MyService()
    {
        _sequentialJobsToRun.Add(new Job1());
        _sequentialJobsToRun.Add(new Job2());
        _sequentialJobsToRun.Add(new Job3());


        _parallelJobsToRun.Add(new Job4());
        _parallelJobsToRun.Add(new Job5());
        _parallelJobsToRun.Add(new Job6());
    }

    public void RunJobs(IProgress<MyCustomProgress> progress)
    {       
        _sequentialJobsToRun.ForEach(job => 
        {
            job.Execute();

            progress.Report(new MyCustomProgress { Current = _jobsToRun.IndexOf(job) + 1, Total = _jobsToRun.Count() });
        });

        Parallel.ForEach(_parallelJobsToRun, job => 
        {
            job.Execute();

            // Report progress here
        });
    }

}

MyCustomProgress ressemble à ceci

public interface IJob
{
    void Execute();
}

et MyService ressemble à quelque chose comme ça ( Job1 , Job2 , Job3 implémentent IJob)

public class MyService : IMyService
{
    private void List<IJob> _jobsToRun;

    public MyService()
    {
        _jobsToRun.Add(new Job1());
        _jobsToRun.Add(new Job2());
        _jobsToRun.Add(new Job3());
    }

    public void RunJobs(IProgress<MyCustomProgress> progress)
    {           
        _jobsToRun.ForEach(job => 
        {
            job.Execute();

            progress.Report(new MyCustomProgress { Current = _jobsToRun.IndexOf(job) + 1, Total = _jobsToRun.Count() });
        });
    }

}


5 commentaires

Quel est l'intérêt d'utiliser Parallel.ForEach sur une liste de tâches? Les tâches s'exécutent déjà en arrière-plan et donc en parallèle. Donc, tout ce que vous faites, c'est gaspiller des threads coûteux en parallèle juste pour lancer un autre thread avec Task.Run. Vous ne devez pas mélanger Parallel.ForEach et les tâches car les deux utilisent de précieuses ressources de thread. Si vous souhaitez que le code s'exécute en parallèle et soit hautement évolutif, utilisez Parallel, si vous souhaitez exécuter du code de manière asynchrone (qui s'exécute également en parallèle), utilisez Task.


Je peux voir pourquoi la confusion. Dans cet exemple, je l'ai appelé Task mais ce n'est pas la tâche C #. J'ai mis à jour l'exemple de code pour mieux articuler cela.


Ces travaux dérivés de "Job " ont-ils même la possibilité de rendre compte de leur progression?


Salut, merci pour le commentaire. J'ai réécrit ma question avec un exemple de code détaillé.


Vous pouvez également essayer d'utiliser Hangire. C'est un moyen simple d'effectuer un traitement en arrière-plan dans les applications .NET et .NET Core. Aucun service Windows ou processus séparé requis. C'est aussi open source.


6 Réponses :


0
votes

Je suis un peu confus par votre description, mais voyons si je comprends ce que vous faites. Donc, si vous enveloppez tout cela dans une classe, alors taskList1 et taskList2 pourraient être des variables de classe. (Au fait, taskList1 / 2 devrait être mieux nommé: disons parallelTaskList et autre chose. Est-ce que cela aide ou ai-je complètement manqué votre question?


1 commentaires

Les itérer nécessiterait que l'application console connaisse les variables de classe possibles, mais je ne suis pas en mesure de le faire. La bibliothèque de classes ne sait pas qui l'appelle et l'application console est indépendante du nombre d'étapes dans la bibliothèque.



0
votes

Pouvez-vous le modifier comme ceci?

public Task StartAsync(CancellationToken cancellationToken, Action<Task,int> onprogress)
{
    _myServiceFromLibrary.RunTasks(onprogress);

    return Task.CompletedTask;
}

public class SimpleProgress : IProgress<int>
{
    private readonly Task task;
    private readonly Action<Task,int> action;
    public SimpleProgress(Task task, Action<Task,int> action)
    {
        this.task = task;
        this.action = action;
    }

    public void Report(int progress)
    {
        action(task, progress);
    }
}

public ICollection<IProgress<int>> RunTasks(Action<Task,int> onprogress)
{
    var taskList1 = new List<ITask> { Task1, Task2 };
    taskList1.foreach(t => t.Progress = new SimpleProgress(t, onprogress));
    var taskList2 = new List<ITask> { Task3, Task4, Task5 }:
    taskList2.foreach(t => t.Progress = new SimpleProgress(t, onprogress));

    taskList1.foreach( task => task.Run() );

    Parallel.Foreach(taskList2, task => { task.Run() });
}

Task.Progress il y a probablement un getter de progression. de manière réaliste, IProgress devrait probablement être injecté via les constructeurs de tâches. Mais le fait est que votre interface publique n'accepte pas la liste de tâches, elle devrait donc simplement renvoyer une collection de rapports d'avancement.

Comment injecter des rapporteurs de progression dans vos tâches est une autre histoire qui dépend de l'implémentation des tâches et il peut ou non être pris en charge. prêt à l'emploi.

Cependant, ce que vous devriez probablement faire est de fournir un rappel de progression ou une usine de progression afin que les rapporteurs de progression de votre choix soient créés:

public Task<ICollection<IProgress<int>>> StartAsync(CancellationToken cancellationToken)
{
    var progressList = _myServiceFromLibrary.RunTasks();

    return Task.FromResult(progressList);
}

public ICollection<IProgress<int>> RunTasks()
{
    var taskList1 = new List<ITask> { Task1, Task2 };
    var plist1 = taskList1.Select(t => t.Progress).ToList();
    var taskList2 = new List<ITask> { Task3, Task4, Task5 }:
    var plist2 = taskList2.Select(t => t.Progress).ToList();

    taskList1.foreach( task => task.Run() );

    Parallel.Foreach(taskList2, task => { task.Run() });

    return plist1.Concat(plist2).ToList();
}

vous pouvez voir ici, qu'il s'agit surtout de savoir comment vos tâches vont appeler la méthode IProgress .Report (T value) .


1 commentaires

Salut, je semble avoir donné la fausse impression dans ma question que mes tâches étaient du type C # Task mais je voulais dire cela au sens littéral. J'ai mis à jour ma question maintenant. Comment modifieriez-vous cette réponse dans cet esprit?



0
votes

Honnêtement, j'utiliserais simplement un événement dans votre prototype de tâche.

Ce que vous voulez n'est pas vraiment clair car le code que vous avez publié ne correspond pas aux noms que vous référencez alors dans le texte de votre question ... Ce serait utile d'avoir tout le code (la fonction RunTasks par exemple, votre prototype IProgress, etc.).

Néanmoins, un événement existe spécifiquement pour signaler le code d'appel. Revenons à l'essentiel. Disons que vous avez une bibliothèque appelée MyLib, avec une méthode DoThings ().

Créez une nouvelle classe qui hérite d'EventArgs, et qui portera les rapports d'avancement de votre tâche ...

OnProgress(new ProgressEventArgs(2452343, 10, "Reindexing google..."));

Ensuite, sur la définition de classe de votre bibliothèque, ajoutez un événement comme ceci:

protected virtual void OnProgress(ProgressEventArgs e)
{
    var handler = Progress;
    if (handler != null)
    {
        handler(this, e);
    }
}

Et dans votre application console, créez un gestionnaire pour les événements de progression: p>

lib.Progress -= ProgressHandler;

Et abonnez-vous à l'événement de la bibliothèque de votre classe:

var lib = new MyLib();
lib.Progress += ProgressHandler;
lib.DoThings();

Lorsque vous avez terminé, désabonnez-vous de l'événement:

void ProgressHandler(object sender, ProgressEventArgs e)
{
    // Do whatever you want with your progress report here, all your
    // info is in the e variable
}

Dans votre bibliothèque de cours, vous pouvez maintenant renvoyer des rapports de progression en déclenchant l'événement dans votre code. Créez d'abord une méthode stub pour appeler l'événement:

public event EventHandler<ProgressEventArgs> Progress;

Et puis ajoutez ceci au code de votre tâche où vous le souhaitez:

public class ProgressEventArgs : EventArgs
{
    private int _taskId;
    private int _percent;
    private string _message;


    public int TaskId => _taskId;
    public int Percent => _percent;
    public string Message => _message;

    public ProgressEventArgs(int taskId, int percent, string message)
    {
        _taskId = taskId;
        _percent = percent;
        _message = message;
    }
}

La seule chose à laquelle il faut faire attention est de signaler les progrès avec parcimonie, car chaque fois que votre événement se déclenche, il interrompt votre application console, et vous pouvez vraiment vous enliser si vous envoyez 10 millions d'événements en même temps. Soyez logique à ce sujet.


4 commentaires

Salut, merci d'avoir répondu. J'ai réécrit ma question avec un exemple de code détaillé.


Bonjour, même avec votre réponse mise à jour, je pense que cette approche convient toujours


Je ne suis pas sûr de comprendre la différence entre cela et l'utilisation de IProgress pour résoudre mon problème principal, à savoir que plusieurs événements de progression sont renvoyés à l'appelant, par Job () depuis _parallelJobsToRun afin que je puisse afficher une barre de progression pour chacun d'entre eux simultanément. Je pense que votre méthode fonctionnerait aussi bien, mais seulement pour un rapport de progression à la fois, mais pas lorsque vous souhaitez obtenir plusieurs mises à jour de progression, non? Ou ai-je mal compris votre implémentation?


Je pense que vous avez manqué la propriété que j'ai mise dans ProgressEventArgs, TaskId. Vous devez identifier vos travaux lors de la création de rapports, mais n'importe quel nombre d'entre eux peut générer des rapports. Cela fait la même chose que vous essayez de faire, mais le modèle standard approuvé par Microsoft.



0
votes

Autre manière; Si vous possédez le code IProgress et Progress

IProgress<T>
{
   IProgress<T> CreateNew();    
   Report(T progress);
}

Progress<T> : IProgress<T>
{
  Progress(ShellProgressClass)
  {
    // initialize progressBar or span new
  }   
  ....
   IProgress<T> CreateNew()
   {
     return new Progress();
   }
}

vous pouvez plus tard improviser pour avoir un grand progressBar (collection de Sequential ou Parallèle) et quoi pas


0 commentaires

0
votes

Votre MyService pourrait avoir une dépendance similaire à:

public class MyService : IMyService
{
    private readonly IJobContainer sequentialJobs;
    private readonly IJobContainer parallelJobs;

    public MyService(
        IJobContainer sequentialJobs,
        IJobContainer parallelJobs)
    {
        this.sequentialJobs = sequentialJobs;
        this.parallelJobs = parallelJobs;

        this.sequentialJobs.Add(new DoSequentialJob1());
        this.sequentialJobs.Add(new DoSequentialJob2());
        this.sequentialJobs.Add(new DoSequentialJob3));

        this.parallelJobs.Add(new DoParallelJobA());
        this.parallelJobs.Add(new DoParallelJobB());
        this.parallelJobs.Add(new DoParallelJobC());
    }

    public void RunJobs(IProgress<MyCustomProgress> progress)
    {
        sequentialJobs.RunJobs(progress, job => 
        {
             // do something with the job if necessary
        });

        parallelJobs.RunJobs(progress, job => 
        {
             // do something with the job if necessary
        });
    }

De cette façon, vous n'avez pas à vous soucier de signaler la progression dans MyService (ce qui ne semble pas être le travail de MyService de toute façon . L'implémentation pourrait ressembler à ceci pour le conteneur de jobs parallèles:

public class MyParallelJobContainer
{
    private readonly IList<IJob> parallelJobs = new List<IJob>();

    public MyParallelJobContainer()
    {
        this.progress = progress;
    }

    public void Add(IJob job) { ... }

    void RunJobs(IProgress<MyProgress> progress, Action<IJob>? callback = null)
    {
        using (var progressBar = new ProgressBar(options...))
        {
            Parallel.ForEach(parallelJobs, job =>
            {
                callback?.Invoke(job);
                job.Execute();
                progressBar.Tick();
            })
        }
    }
}

MyService ressemblerait alors à ceci:

public interface IJobContainer
{
    void Add(IJob job);

    void RunJobs(IProgress<MyProgress> progress, Action<IJob>? callback = null); // Using an action for extra work you may want to do
}

L'avantage de cette méthode est que MyService n'a qu'un seul travail et n'a pas à se soucier de ce que vous faites une fois le travail terminé.


1 commentaires

Merci de répondre. J'aime la direction avec ceci, mais je pense que cela a la même limitation que ma question initiale. Lorsque les travaux parallèles sont exécutés, il passe à Tick () lorsque chaque travail est terminé. Ce qui signifie que IProgress ne pourra pas rapporter la progression de chaque IJob individuel.



0
votes

D'après ce que j'ai compris de votre problème, la question est de savoir comment afficher la progression à travers à la fois l'achèvement des travaux synchrones et des travaux parallèles.

En théorie, les travaux parallèles pourraient commencer et se terminer au même temps, vous pouvez donc traiter les travaux parallèles comme un travail unique. Au lieu d'utiliser le nombre de travaux séquentiels comme total, augmentez ce nombre de un. Cela peut être satisfaisant pour un petit nombre de travaux parallèles.

Si vous souhaitez ajouter une progression entre les travaux parallèles, vous devrez gérer le multi-threading dans votre code car les travaux parallèles s'exécuteront simultanément .

object pJobLock = new object();
int numProcessed = 0;
foreach(var parallelJob in parallelJobs)
{
    parallelJob.DoWork();
    lock (pJobLock)
    {
        numProcessed++;
        progress.Report(new MyCustomProgress { Current = numProcessed, Total = parallelJobs.Count() });
    }
}


0 commentaires