.net core — передача неизвестного числа IProgress<T> в библиотеку классов

У меня есть консольное приложение, которое использует библиотеку классов для выполнения некоторых длительных задач. Это консольное приложение .net core, использующее универсальный хост .net core. Я также использую библиотеку ShellProgressBar для отображения некоторых индикаторов выполнения.

Мой хостинг выглядит так

internal class MyHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private readonly IMyService _myService;
    private readonly IProgress<MyCustomProgress> _progress;
    private readonly IApplicationLifetime _appLifetime;
    private readonly ProgressBar _progressBar;
    private readonly IProgressBarFactory _progressBarFactory;

    public MyHostedService(
        ILogger<MyHostedService> logger, 
        IMyService myService,
        IProgressBarFactory progressBarFactory,
        IApplicationLifetime appLifetime)
    {
        _logger = logger;
        _myService = myService;
        _appLifetime = appLifetime;
        _progressBarFactory = progressBarFactory;

        _progressBar = _progressBarFactory.GetProgressBar();        // this just returns an instance of ShellProgressBar

        _progress = new Progress<MyCustomProgress>(progress =>
        {
            _progressBar.Tick(progress.Current);
        });
    }

    public void Dispose()
    {
        _progressBar.Dispose();
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _myService.RunJobs(_progress);
        _appLifetime.StopApplication();

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

Где MyCustomProgress выглядит так

public class MyCustomProgress
{
    public int Current {get; set;}
    public int Total {get; set;}
}

а MyService выглядит примерно так (Job1, Job2, Job3 реализовать 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() });
        });
    }

}

И IJob есть

public interface IJob
{
    void Execute();
}

Эта настройка работает хорошо, и я могу отобразить индикатор выполнения из своего HostedService, создав экземпляр ShellProgressBar и используя один экземпляр IProgress, который мне нужно обновить.

Однако у меня есть другая реализация IMyService, которую мне также нужно запустить, которая выглядит примерно так:

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

}

Это тот, с которым я борюсь. когда _parallelJobsToRun выполняется, мне нужно иметь возможность создать новый дочерний элемент ShellProgressBar (ShellProgressBar.Spawn) и отображать их как дочерние индикаторы выполнения, скажем, «Параллельные задания».

Вот где я ищу помощь в том, как я могу этого добиться.

Примечание. Я не хочу зависеть от ShellProgressBar в моей библиотеке классов, содержащей MyService

Любая помощь очень ценится.

Какой смысл использовать Parallel.ForEach для списка задач? Задачи уже выполняются в фоновом режиме и, следовательно, параллельно. Таким образом, все, что вы делаете, это тратите дорогостоящие потоки в Parallel только для того, чтобы запустить другой поток с помощью Task.Run. Не следует смешивать Parallel.ForEach и задачи, так как оба они используют ценные ресурсы потоков. Если вы хотите, чтобы код выполнялся параллельно и был хорошо масштабируемым, используйте Parallel, если вы хотите выполнять код асинхронно (который также выполняется параллельно), используйте Task.

ckuri 16.02.2019 08:02

Я понимаю, почему путаница. В этом примере я назвал его Task, но это не задача C#. Я обновил пример кода, чтобы лучше сформулировать это.

reggaemahn 16.02.2019 08:43

Имеют ли эти производные задания "IJob" возможность сообщать о своем прогрессе?

Nkosi 20.02.2019 02:25

Привет, спасибо за комментарий. Я переписал свой вопрос с подробным примером кода.

reggaemahn 20.02.2019 07:32

В качестве альтернативы вы можете попробовать использовать Hangire. Это простой способ выполнения фоновой обработки в приложениях .NET и .NET Core. Служба Windows или отдельный процесс не требуются. Это тоже с открытым исходным кодом.

Mohit Verma 21.02.2019 10:36
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
5
584
6

Ответы 6

Меня немного смущает ваше описание, но давайте посмотрим, понимаю ли я, что вы задумали. Итак, если вы обернете все это в класс, тогда taskList1 и taskList2 могут быть переменными класса. (Кстати, taskList1/2 должен называться лучше: скажем, parallelTaskList и что угодно... в любом случае.) Затем вы можете написать новый метод для класса CheckTaskStatus() и просто перебирать две переменные класса. Это помогает или я полностью пропустил ваш вопрос?

Итерация по ним потребует, чтобы консольное приложение знало о возможных переменных класса, но я не могу этого сделать. Библиотека классов не зависит от того, кто ее вызывает, а консольное приложение не зависит от количества шагов в библиотеке.

reggaemahn 17.02.2019 21:22

Можете ли вы изменить его так?


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

Task.Progress наверное есть геттер прогресса. на самом деле IProgress, вероятно, следует внедрять через конструкторы задач. Но дело в том, что ваш публичный интерфейс не принимает список задач, поэтому он должен просто возвращать набор отчетов о ходе выполнения.

Как внедрить отчеты о прогрессе в ваши задачи — это отдельная история, которая зависит от реализации задач, и она может поддерживаться или не поддерживаться. из коробки.

Однако то, что вы, вероятно, должны сделать, это предоставить обратный вызов прогресса или фабрику прогресса, чтобы создавать отчеты о прогрессе по вашему выбору:


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

Вы можете видеть здесь, что на самом деле это в основном вопрос о том, как ваши задачи будут вызывать метод IProgress<T>.Report(T value).

Привет, кажется, в моем вопросе у меня сложилось ложное впечатление, что мои задачи были типа C# Task, но я имел в виду это в буквальном смысле. Я обновил свой вопрос сейчас. Как бы вы изменили этот ответ с учетом этого?

reggaemahn 17.02.2019 21:20

Честно говоря, я бы просто использовал событие в прототипе вашей задачи.

На самом деле не совсем ясно, чего вы хотите, потому что опубликованный вами код не соответствует именам, которые вы затем ссылаетесь в тексте своего вопроса... Было бы полезно иметь весь код (например, функцию RunTasks, ваш прототип IProgress и т. д.). ).

Тем не менее, событие существует специально для сигнализации о вызывающем коде. Вернемся к основам. Допустим, у вас есть библиотека MyLib с методом DoThings().

Создайте новый класс, который наследуется от EventArgs и будет содержать отчеты о ходе выполнения вашей задачи...

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

Затем в определении класса вашей библиотеки добавьте такое событие:

public event EventHandler<ProgressEventArgs> Progress;

И в своем консольном приложении создайте обработчик событий прогресса:

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

И подпишитесь на событие вашей библиотеки классов:

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

Когда вы закончите, отпишитесь от события:

lib.Progress -= ProgressHandler;

В вашей библиотеке классов теперь вы можете отправлять отчеты о ходе выполнения, вызывая событие в своем коде. Сначала создайте метод-заглушку для вызова события:

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

А затем добавьте это в код своей задачи, где вы хотите:

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

Единственное, с чем нужно быть осторожным, — это скупо сообщать о прогрессе, потому что каждый раз, когда ваше событие запускается, оно прерывает ваше консольное приложение, и вы можете сильно затормозить его, если отправите 10 миллионов событий одновременно. Будьте логичны в этом.

Привет, спасибо за ответ. Я переписал свой вопрос с подробным примером кода.

reggaemahn 20.02.2019 07:31

Привет, даже с вашим обновленным ответом, я думаю, что этот подход по-прежнему отвечает всем требованиям.

Drunken Code Monkey 20.02.2019 15:00

Я не уверен, что понимаю разницу между этим и использованием IProgress для решения моей основной проблемы, которая заключается в отправке нескольких событий выполнения обратно вызывающей стороне, на Job() из _parallelJobsToRun, чтобы я мог отображать индикатор выполнения для каждого из них одновременно. Я думаю, что ваш метод тоже сработает, но только для одного отчета о ходе выполнения за раз, а не тогда, когда вы хотите получать несколько обновлений о ходе выполнения, верно? Или я неправильно понял вашу реализацию?

reggaemahn 21.02.2019 02:44

Я думаю, вы пропустили свойство, которое я добавил в ProgressEventArgs, TaskId. При составлении отчетов вам необходимо указать свои рабочие места, но любое их количество может сообщаться. Это делает то же самое, что вы пытаетесь сделать, но по стандартному шаблону, одобренному Microsoft.

Drunken Code Monkey 21.02.2019 02:48

Альтернативный способ; Если у вас есть код IProgress<T> и 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();
   }
}

позже вы можете импровизировать, чтобы иметь один большой progressBar (набор Sequential или Parallel), а что нет

Ваш MyService может иметь зависимость, подобную:

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
}

Таким образом, вам не нужно беспокоиться об отчетах о прогрессе в MyService (что в любом случае не похоже на то, что это должно быть заданием MyService. Реализация для контейнера параллельных заданий может выглядеть примерно так:

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 будет выглядеть так:

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

Преимущество этого способа заключается в том, что у MyService есть только одно задание, и ему не нужно беспокоиться о том, что вы будете делать после завершения задания.

Спасибо за ответ. Мне нравится направление с этим, однако я думаю, что это имеет то же ограничение, что и мой первоначальный вопрос. Когда параллельные задания выполняются, это происходит Tick(), когда каждое задание завершено. Это означает, что IProgress не сможет сообщать о прогрессе каждого человека IJob.

reggaemahn 24.02.2019 23:03

Насколько я понимаю вашу проблему, вопрос заключается в том, как вы отображаете ход выполнения оба выполнения синхронных и параллельных заданий.

Теоретически параллельные задания могут начинаться и заканчиваться одновременно, поэтому вы можете рассматривать параллельные задания как одно задание. Вместо того, чтобы использовать количество последовательных заданий в качестве общего, увеличьте это число на единицу. Этого может быть достаточно для небольшого количества параллельных заданий.

Если вы хотите добавить прогресс между параллельными заданиями, вам нужно будет обрабатывать многопоточность в своем коде, потому что параллельные задания будут выполняться одновременно.

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

Другие вопросы по теме