【问题标题】:.net core - Passing an unknown number of IProgress<T> to class library.net core - 将未知数量的 IProgress<T> 传递给类库
【发布时间】:2019-02-16 00:33:32
【问题描述】:

我有一个控制台应用程序,它使用类库来执行一些长时间运行的任务。这是一个 .net core 控制台应用程序,使用 .net core Generic Host。我还使用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 看起来像这样(Job1Job2Job3 实现 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();
}

此设置运行良好,我可以通过创建ShellProgressBar 实例并使用我必须更新的IProgress 实例来显示我的HostedService 的进度条。

但是,我还需要运行 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) 并将它们显示为让我们说'并行作业'的子进度条。

这是我正在寻求一些帮助以了解如何实现这一点的地方。

注意:我不想在包含 MyService 的类库中依赖 ShellProgressBar

非常感谢任何帮助。

【问题讨论】:

  • 在任务列表上使用 Parallel.ForEach 有什么意义?任务已经在后台运行,因此是并行的。因此,您所做的就是在 Parallel 中浪费昂贵的线程,只是为了使用 Task.Run 启动另一个线程。您不应该混合使用 Parallel.ForEach 和任务,因为它们都使用宝贵的线程资源。如果您希望代码并行运行并具有高度可扩展性,请使用 Parallel,如果您希望异步运行代码(也可以并行运行),请使用 Task。
  • 我明白为什么会出现这种混乱。在此示例中,我将其命名为 Task,但它不是 C# 任务。我已经更新了示例代码以更好地表达这一点。
  • 这些 "IJob" 派生的工作是否有能力报告他们的进度?
  • 您好,感谢您的评论。我用详细的示例代码重写了我的问题。
  • 或者,您可以尝试使用 Hangire。这是在 .NET 和 .NET Core 应用程序中执行后台处理的简单方法。无需 Windows 服务或单独的进程。它也是开源的。

标签: c# .net asp.net-core .net-core


【解决方案1】:

我对你的描述有点困惑,但让我们看看我是否理解你在做什么。因此,如果您将所有这些包装在一个类中,那么 taskList1 和 taskList2 可能是类变量。 (顺便说一下,taskList1/2 应该更好地命名:比如parallelTask​​List 等等……无论如何。)然后你可以在类CheckTaskStatus() 上编写一个新方法,然后迭代两个类变量。这有帮助还是我完全错过了你的问题?

【讨论】:

  • 对它们进行迭代需要控制台应用程序了解可能的类变量,但我无法做到这一点。类库不知道是谁在调用它,控制台应用也不知道类库中有多少步骤。
【解决方案2】:

你可以这样修改吗?


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 可能应该通过 Tasks 构造函数注入。但关键是您的公共界面不接受任务列表,因此它应该只返回进度报告集合。

如何将进度报告器注入您的任务是另一回事,这取决于任务的实现,它可能受支持,也可能不受支持。开箱即用。

但是您可能应该做的是提供进度回调或进度工厂,以便创建您选择的进度报告器:


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&lt;T&gt;.Report(T value) 方法的问题。

【讨论】:

  • 嗨,我似乎在我的问题中给人一种错误的印象,即我的任务是 C# Task 类型,但我的意思是字面意思。我现在更新了我的问题。考虑到这一点,您将如何修改此答案?
【解决方案3】:

老实说,我只想在你的任务原型中使用一个事件。

不清楚您想要什么,因为您发布的代码与您在问题文本中引用的名称不匹配...拥有所有代码(例如 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..."));

唯一需要注意的是谨慎地报告进度,因为每次触发事件都会中断控制台应用程序,如果一次发送 1000 万个事件,您真的会陷入困境。保持逻辑。

【讨论】:

  • 您好,感谢您的回答。我用详细的示例代码重写了我的问题。
  • 嗨,即使您更新了答案,我认为这种方法仍然符合要求
  • 我不确定我是否理解这与使用IProgress 解决我的核心问题之间的区别,即多个进度事件被发送回调用者,每个Job() 来自_parallelJobsToRun这样我就可以同时为每个人显示一个进度条。我认为您的方法也可以,但一次只能用于一个进度报告,而不是当您想要获得多个进度更新时,对吗?还是我误解了你的实现?
  • 我想你错过了我放在 ProgressEventArgs 中的属性,TaskId。您需要在报告时确定您的工作,但任何数量的工作都可以报告。这与您尝试做的事情相同,但标准的、Microsoft 批准的模式。
【解决方案4】:

替代方式;如果您拥有代码IProgress&lt;T&gt;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();
   }
}

你以后可以即兴创作一个大的进度条(顺序或并行的集合),什么不是

【讨论】:

    【解决方案5】:

    您的 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 的进度。
    【解决方案6】:

    根据我对您的问题的理解,问题是您如何显示同步作业和并行作业的完成的进度。

    理论上,并行作业可以同时开始和结束,因此您可以将并行作业视为单个作业。不要使用连续作业的计数作为总数,而是将该数字增加一。这对于少量并行作业可能是令人满意的。

    如果您想在并行作业之间添加进度,则需要在代码中处理多线程,因为并行作业将同时运行

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-05-06
      • 1970-01-01
      • 2018-08-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多