我有一些UI代码,我有一个如下所示的方法:
private async Task UpdateStatusAsync()
{
//Do stuff on UI thread...
var result = await Task.Run(DoBunchOfStuffInBackground);
//Update UI based on result of background processing...
}
目标是UI在任何影响其状态的属性更改时更新相对复杂的计算状态。这里有一些问题:
我正在寻找的是一个干净的模式,可以实现以下目标:
看起来这应该是一个相当普遍的问题,所以我想我会问这里是否有人知道这样做的特别干净。
答案 0 :(得分:2)
嗯,最简单的方法是使用CancellationToken
取消旧的状态更新,使用Task.Delay
来延迟状态更新:
private CancellationTokenSource cancelCurrentUpdate;
private Task currentUpdate;
private async Task UpdateStatusAsync()
{
//Do stuff on UI thread...
// Cancel any running update
if (cancelCurrentUpdate != null)
{
cancelCurrentUpdate.Cancel();
try { await currentUpdate; } catch (OperationCanceledException) { }
// or "await Task.WhenAny(currentUpdate);" to avoid the try/catch but have less clear code
}
try
{
cancelCurrentUpdate = new CancellationTokenSource();
var token = cancelCurrentUpdate.Token;
currentUpdate = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMilliseconds(100), token);
DoBunchOfStuffInBackground(token);
}, token);
var result = await currentUpdate;
//Update UI based on result of background processing...
}
catch (OperationCanceledException) { }
}
如果您正在快速更新 ,那么这种方法会为GC 和创建(甚至)更多垃圾,这种简单方法将始终取消较旧的状态更新,如果没有' ta" break"在这些事件中,用户界面最终可能会落后。
这种复杂程度是async
开始达到极限的地方。 Reactive extensions如果您需要更复杂的东西(例如处理"中断"那么您至少每隔一段时间就会获得一次UI更新),这将是一个更好的选择。 Rx在处理时机方面特别擅长。
答案 1 :(得分:1)
您应该可以在不使用计时器的情况下执行此操作。一般来说:
private async Task UpdateStatusAsync()
{
//Do stuff on UI thread...
set update pending flag
if currently doing background processing
{
return
}
while update pending
{
clear update pending flag
set background processing flag
result = await Task.Run(DoBunchOfStuffInBackground);
//Update UI based on result of background processing...
}
clear background processing flag
}
我必须思考如何在async / await的上下文中完成所有这些操作。我过去曾做过与BackgroundWorker
类似的事情,所以我知道这是可能的。
防止它丢失更新应该很容易,但它可能会不时地进行不必要的后台处理。但是,当在短时间内发布10个更新时,它肯定会消除9次不必要的更新(可能是第一次和最后一次)。
如果需要,可以将UI更新移出循环。取决于你是否介意看到中间更新。
答案 2 :(得分:0)
由于我似乎走在正确的轨道上,我会提出我的建议。在非常基本的伪代码中,看起来这可能起到了作用:
int counter = 0;
if (update received && counter < MAX_ITERATIONS)
{
store info;
reset N_MILLISECOND timer;
}
if (timer expires)
{
counter = 0;
do calculation;
}
这样您就可以跳过任意数量太多的电话,而计数器会确保您仍然保持用户界面的最新状态。
答案 3 :(得分:0)
我最后使用Jim Mischel建议的方法,添加了一个计时器来汇总快速传入的触发器:
public sealed class ThrottledTask
{
private readonly object _runLock = new object();
private readonly Func<Task> _runTask;
private Task _loopTask;
private int _updatePending;
public ThrottledTask(Func<Task> runTask)
{
_runTask = runTask;
AggregationPeriod = TimeSpan.FromMilliseconds(10);
}
public TimeSpan AggregationPeriod { get; private set; }
public Task Run()
{
_updatePending = 1;
lock (_runLock)
{
if (_loopTask == null)
_loopTask = RunLoop();
return _loopTask;
}
}
private async Task RunLoop()
{
//Allow some time before we start processing, in case many requests pile up
await Task.Delay(AggregationPeriod);
//Continue to process as long as update is still pending
//This clears flag on each cycle in a thread-safe way
while (Interlocked.CompareExchange(ref _updatePending, 0, 1) == 1)
{
await _runTask();
}
lock (_runLock)
{
_loopTask = null;
}
}
}
只要仍有传入触发器,一旦聚合期过去,它就会尽快运行更新。关键是如果触发器发生的速度比计算速度快,则不会叠加冗余更新,并且始终确保“最后”触发器获得更新。