如何获取具有许多任务的异步函数的进度报告,其中我使用 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);

提问人:Tratak 提问时间:10/11/2023 最后编辑:Theodor ZouliasTratak 更新时间:10/11/2023 访问量:125

问:

我已经阅读了所有这些帖子:

报告异步任务的进度

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

所以我想在任何打开时进行更改。不知何故,每个任务都必须有一个名称,并且该名称要报告给变量。StatusMessageTask1..Task5StatusMessage

我还希望事件在变化时上升。StatusMessage

StatusMessage也可以是一个累积列表,或者只是一个字符串。

JSteward从这里发帖很有趣,但我不知道如何转换我的Task1..Task5 任务来自他的样本。WorkItem

我该怎么做?

C# 异步 async-await maui 进度

评论

1赞 Theodor Zoulias 10/11/2023
您确定此进度行为对用户有用吗?所有任务 、 、 和 可能大致同时完成。换句话说,您似乎要报告任务完成情况,而不是进度。我的理解是,进展应该以数字 0 - 100 的形式报告。Task1Task2Task3Task4Task5
1赞 Theodor Zoulias 10/11/2023
应用程序的类型是什么?WinForms的?ASP.NET?
1赞 Good Night Nerd Pride 10/11/2023
尝试添加到列表中的每个条目。.ContinueWith(() => StatusMessage = "Task X done")
1赞 Tratak 10/11/2023
@Theodor Zoulias 这是一个 MAUI 应用程序。我想用sqlite dayabase同步中央数据库中的一些表。我想将这些信息从初始屏幕发送到标签。

答:

0赞 JonasH 10/11/2023 #1

如果您想进行更详细的进度报告,该标准有点局限性。没有简单的方法可以包含描述等功能或将工作分布到多个独立任务上。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))

即使每个方法在不同的任务或线程上运行,这也应该有效。

3赞 Theodor Zoulias 10/11/2023 #2

可以在类型上定义一个扩展方法,该方法在任务成功完成时报告 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> 类型以异步方式报告进度。这意味着可以在方法完成后调用。如果直接在 上写任何内容,则紧跟在行之后,此文本可能不是附加到 .如果您不喜欢这种行为,可以在此处找到同步实现。handlerButtonClickHandlertextBox1await Task.WhenAll(tasks);textBox1IProgress<T>

评论

0赞 Sir Rufo 10/17/2023
只有在阻止异步代码时才应该遇到这个问题,而你无论如何都不应该这样做Progress<T>
0赞 Theodor Zoulias 10/17/2023
@SirRufo查看此演示。使用 ,消息显示在 报告的消息之前,这可能并不可取。使用 ,按预期最后显示。Progress<T>"Done"progressSynchronousProgress<T>"Done"