20

用于IProgress<T>报告进度时,应该是

  • 代码报告进度的责任是将其进度报告限制为“合理”的频率,-或-
  • 具体实施的责任IProgress<T>是意识到进展报告的频率可能高于其呈现进展的方式的合理频率。

问题的上下文是我有一些IProgress<T>用于报告进度的代码,它以非常高的速度报告进度。我想用 UI 进度条显示进度。如果我使用提供的Progress<T>实现(将进度发布到 UI SyncronizationContext),那么它会导致 UI 无响应(即发送到消息队列的消息太多,用户甚至无法单击“取消”按钮在对话框中)。

所以,

  • 我可以通过减少报告来解决这个问题,但是如果我有一个IProgress<T>只是将进度写入日志文件的实现(并且可以处理高报告频率)怎么办。-或者-
  • 我可以通过创建自己的特定IProgress<T>实现来解决这个问题,该实现限制了我处理/报告进度的频率。据推测,此实现将记录非 UI 线程上的最新进度,然后(可能)UI 将根据计时器进行更新。
4

2 回答 2

17

编写一个装饰器来限制调用。通过这种方式,您可以将限制逻辑和实际报告分开,您可以将其用于任何其他IProgress<T>实现。

当你想限制进度报告时使用这个装饰器。用下面的类的实例简单地包装你的进度报告器。

我已经把节流逻辑留给你了。您可以使其基于时间、基于呼叫量或其他一些标准。

public class ProgressThrottler<T>: IProgress<T> {
    public ProgressThrottler(IProgress<T> progress) {
        _progress = progress ?? throw new ArgumentNullException("progress");
    }

    private readonly IProgress<T> _progress;

    public void Report(T value) {
        // Throttles the amount of calls
        bool reportProgressAfterThrottling = ...;

        if (reportProgressAfterThrottling) {
            _progress.Report(value);
        }
    }
}
于 2013-10-29T14:43:06.827 回答
-2

这是一个自定义Progress<T>实现,它在连续进度报告之间强制执行最小间隔策略。发出报告消息时,它会启动一段静默期,在此期间所有后续报告消息都将被丢弃(忽略),除了最后一条消息。每个周期的最后一条报告消息在周期结束时被缓冲并发出,然后启动一个新的静默期等。每个静默期的持续时间是可配置的(dueTime参数)。

public class ThrottledProgress<T> : Progress<T>
{
    private readonly TimeSpan _dueTime;
    private readonly object _locker = new object();
    private (T Value, bool HasValue) _current;
    private Task _task;

    public ThrottledProgress(Action<T> handler, TimeSpan dueTime) : base(handler)
    {
        if (dueTime < TimeSpan.Zero || dueTime.TotalMilliseconds > Int32.MaxValue)
            throw new ArgumentOutOfRangeException(nameof(dueTime));
        _dueTime = dueTime;
    }

    protected override void OnReport(T value)
    {
        lock (_locker)
        {
            if (_task == null)
            {
                base.OnReport(value);
                _task = Task.Run(async () =>
                {
                    while (true)
                    {
                        await Task.Delay(_dueTime);
                        lock (_locker)
                        {
                            if (_current.HasValue)
                            {
                                base.OnReport(_current.Value);
                                _current = (default, false);
                            }
                            else
                            {
                                _task = null;
                                break;
                            }
                        }
                    }
                });
            }
            else
            {
                _current = (value, true);
            }
        }
    }

    public void Flush()
    {
        lock (_locker)
        {
            if (_current.HasValue)
            {
                base.OnReport(_current.Value);
                _current = (default, false);
            }
        }
    }
}

使用示例,基于最近重复问题中发布的代码:

async void Button_Click(object sender, RoutedEventArgs e)
{
    _cts = new CancellationTokenSource();
    var progress = new ThrottledProgress<string>(msg => TextBox.Text += msg,
        TimeSpan.FromMilliseconds(50));
    var tasks = Enumerable.Range(1, 10)
        .Select(i => Task.Run(() => Worker(i, _cts.Token, progress)));
    await Task.WhenAll(tasks);
    progress.Flush();
}

可以在异步操作完成后调用该Flush方法,以便任何可能仍被缓冲并计划将来发送的进度消息(可能是最后一个进度消息)立即发出。

于 2021-07-07T20:20:28.247 回答