提问人:Tratak 提问时间:10/11/2023 最后编辑:Theodor ZouliasTratak 更新时间:10/11/2023 访问量:125
如何获取具有许多任务的异步函数的进度报告,其中我使用 await Task.WhenAll(tasks);
How can I get a progress report on a async function with many tasks, in which I am using await Task.WhenAll(tasks);
问:
我已经阅读了所有这些帖子:
https://devblogs.microsoft.com/dotnet/async-in-4-5-enabling-progress-and-cancellation-in-async-apis/
https://blog.stephencleary.com/2012/02/reporting-progress-from-async-tasks.html
但是当我使用时,我仍然不知道如何报告许多任务的进度await Task.WhenAll(tasks);
所以,我正在使用这段代码:
public string StatusMessage { get; set; }
public async void ButtonClickHandler(object sender, EventArgs e)
{
var tasks = new List<Task>
{
Task1,
Task2,
Task3,
Task4,
Task5
};
await Task.WhenAll(tasks);
}
所以我想在任何打开时进行更改。不知何故,每个任务都必须有一个名称,并且该名称要报告给变量。StatusMessage
Task1..Task5
StatusMessage
我还希望事件在变化时上升。StatusMessage
StatusMessage
也可以是一个累积列表,或者只是一个字符串。
JSteward从这里发帖很有趣,但我不知道如何转换我的Task1..Task5 任务来自他的样本。WorkItem
我该怎么做?
答:
如果您想进行更详细的进度报告,该标准有点局限性。没有简单的方法可以包含描述等功能或将工作分布到多个独立任务上。IProgress<T>
我的偏好是将其替换为自定义进度类,例如
public class Progress
{
// object should *either* have value+description, *or* subProgress list
private double value;
private string description = "";
private Progress[] subProgress;
// progress should always be between 0-1 to keep math simple
public double Value
{
get
{
var subProgressLocal = subProgress;
if (subProgress != null)
{
return subProgressLocal.Sum(s => s.Value ) / subProgressLocal.Length;
}
return value;
}
}
public string Description
{
get
{
var subProgressLocal = subProgress;
if (subProgressLocal != null)
{
return subProgressLocal.FirstOrDefault(p => p.Value is > 0d and < 1d)?.Description ?? "";
}
return description;
}
}
public Progress[] CreateSubProgress(int count)
{
if (subProgress != null)
throw new InvalidOperationException("Can only create sub-progresses once");
subProgress = Enumerable.Range(0, count).Select(_ => new Progress()).ToArray();
return subProgress;
}
// progress should always be between 0-1 to keep math simple
public void Report(double progress, string description = "")
{
if (subProgress != null)
throw new InvalidOperationException("Cannot report progress when using sub-progress");
// Clamp to 0-1 range
this.value = progress < 0 ? 0 : (progress > 1 ? 1 : progress);
this.description = description;
}
}
这允许您构建与您正在执行的计算相匹配的“树”,其中所有报告都在叶节点上完成。因此,您可以拥有多个嵌套循环,同时仍能获得详细的进度报告,而无需在开始之前知道总工作量。
当任务报告进度时,这不会在 UI 线程上引发任何事件。因此,您需要使用与 UI 兼容的计时器来定期轮询进度并更新 UI。另请注意,这并不是严格意义上的线程安全,因为共享变量将从不同的线程读取和写入,但可能发生的最坏情况应该是进度条略微滞后。另请注意,这是一个“一次性使用”对象,每次需要进度报告时都会创建一个新对象。
用法如下:
void DoMultiStepWork(Progress p){
var subprogress = p.CreateSubProgress(3);
DoPart1(subProgress[0]);
DoPart2(subProgress[1]);
DoPart3(subProgress[2]);
}
void DoPart1(Progress p){
for(int i = 0; i < myCollection.Count; i++){
// do work
p.Report(i / (double)myCollection.Count, myCollection[i].Name);
}
}
您可以编写扩展方法来使使用更方便,例如 .或者为每个子进度添加权重等功能,以处理工作分布不均匀的情况。foreach(var item in myCollection.ProgressReporting(p))
即使每个方法在不同的任务或线程上运行,这也应该有效。
可以在类型上定义一个扩展方法,该方法在任务成功完成时报告 IProgress<T>
的进度:Task
public static async Task OnSuccessfulCompletion<T>(
this Task task, IProgress<T> progress, T progressValue)
{
ArgumentNullException.ThrowIfNull(task);
ArgumentNullException.ThrowIfNull(progress);
await task.ConfigureAwait(false);
progress.Report(progressValue);
}
并像这样使用它:
public async void ButtonClickHandler(object sender, EventArgs e)
{
/* Launching the tasks Task1, Task2 etc is omitted */
Progress<string> progress = new(value => textBox1.AppendText($"{value}\r\n"));
List<Task> tasks = new()
{
Task1.OnSuccessfulCompletion(progress, "Task1"),
Task2.OnSuccessfulCompletion(progress, "Task2"),
Task3.OnSuccessfulCompletion(progress, "Task3"),
Task4.OnSuccessfulCompletion(progress, "Task4"),
Task5.OnSuccessfulCompletion(progress, "Task5"),
};
await Task.WhenAll(tasks);
}
请注意,Progress<T>
类型以异步方式报告进度。这意味着可以在方法完成后调用。如果直接在 上写任何内容,则紧跟在行之后,此文本可能不是附加到 .如果您不喜欢这种行为,可以在此处找到同步实现。handler
ButtonClickHandler
textBox1
await Task.WhenAll(tasks);
textBox1
IProgress<T>
评论
Progress<T>
Progress<T>
"Done"
progress
SynchronousProgress<T>
"Done"
评论
Task1
Task2
Task3
Task4
Task5
.ContinueWith(() => StatusMessage = "Task X done")