2013-03-13 139 views
3

我有,我有,看起來像這樣的方法的一些UI代碼:避免冗餘異步calcluations

private async Task UpdateStatusAsync() 
    { 
     //Do stuff on UI thread... 

     var result = await Task.Run(DoBunchOfStuffInBackground); 

     //Update UI based on result of background processing... 
    } 

的目標是爲用戶界面更新相對複雜的計算狀態的任何時間屬性改變了影響其州。這裏有幾個問題:

  1. 如果我只是從每個更新狀態的地方直接調用此方法,則最終更新狀態可能不正確。假設屬性A改變,然後屬性B改變。即使B在A之後調用UpdateStatusAsync,有時候回調代碼(最終的UI更新)也會以相反的順序發生。因此:(A - >更新) - >(B - >更新) - >(B更新) - >(A更新)。這意味着最終用戶界面顯示一個陳舊的狀態(反映A,但不是B)。
  2. 如果我總是先等待先前的UpdateStatusAsync完成(我現在正在做的),那麼我可以多次執行昂貴的狀態計算。理想情況下,我只需要對一系列更新進行「最後」計算。

我正在尋找的是一個乾淨的圖案完成以下操作:

  1. 最終地位不應該超過一個小的時間更多(即我不想讓「過時」 UI與底層狀態不同步)
  2. 如果在短時間內出現多個更新調用(一種常見用例),我寧願避免重複工作,而是總是計算「最新」更新。
  3. 由於有幾種情況下可能會在非常接近的時間內(即幾毫秒內)發生多個更新,因此在避免其他更新請求進入時避免啓動處理很方便。

看起來這應該是一個相當普遍的問題,所以我想我會問在這裏是否有人知道這樣做的一個特別乾淨的方式。

+0

是不是這麼簡單:'if(update received){store info;重置100ms定時器}; if(timer expires){do calculation};'? – 2013-03-13 21:42:20

+0

@Scott,類似這樣的東西可能會起作用,儘管它可能會變得非常陳舊(例如,我可以每隔10ms更新一次,例如,在UI狀態開始更新之前需要一整秒) m試圖找到避免冗餘計算和保持UI最新的平衡。 – 2013-03-13 21:47:47

+0

如果你想保持這個一般想法,只需增加一點點複雜性:'if(update received && counter 2013-03-13 22:01:03

回答

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更新移出循環。取決於是否介意看到中間更新。

+0

我使用了類似於這種方法的事情(添加了一個定時器來延遲啓動任務)。 – 2013-06-04 23:16:31

0

既然看起來我在正確的軌道上,我會提交我的建議。在非常基本的僞代碼,它看起來像這可能做的伎倆:

int counter = 0; 

if (update received && counter < MAX_ITERATIONS) 
{ 
    store info; 
    reset N_MILLISECOND timer; 
} 
if (timer expires) 
{ 
    counter = 0; 
    do calculation; 
} 

這將讓你跳過儘可能多地調用過於接近對方,只要你想,當計數器將確保你仍然讓UI保持最新狀態。

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 創建(甚至)更多的垃圾,這種簡單的方法將始終取消舊的狀態更新,因此如果事件中沒有「中斷」,UI最終可能落後於後面。

這個複雜程度是async開始達到極限。 Reactive extensions如果你需要更復雜的東西(比如處理那個「中斷」,所以你至少每次都得到一次UI更新)將會是更好的選擇。 Rx特別擅長處理時間。

0

我清盤使用吉姆·米契爾推薦的方式,以增加快速進入觸發器聚集的計時器:

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

這將運行更新儘可能快地在累積的時期已經過去,只要仍然有傳入的觸發器。關鍵是,如果觸發器發生的速度比計算快,並且它總是確保'最後'觸發器獲得更新,則這不會疊加冗餘更新。