WinForms Multithreading: Execute a GUI update only if the previous one has finished

余生颓废 提交于 2019-12-23 17:53:29

问题


I have multithreaded application with some background processing. It has long-running UI updates (on the UI-thread itself), which are invoked from the background thread via the well-known resource on MSDN. I can not shorten these UI updates since they are finally done in a external library(1).

Now, from that background thread, I want to asynchronously invoke (using BeginInvoke()) these updates on the UI thread, but only if the previous update has finished yet. If not, I would like to simply skip this update. This will prevent an overflow of the Windows Message Queue, in case the invokes come faster than the to invoked method is able to execute.

My current solution is this: In the method that executes on the UI thread, I do enter and exit a ReaderWriterLockSlim instance. On the background thread, I try to enter the instance with zero timeout. When successful, I call 'BeginInvoke()' then exit again. When not successful, I skip the method invoke altogether.

public void Update(double x, double y)
{
    _updateLock.EnterWriteLock();
    try
    { //...long running task... }
    finally
    { _updateLock.ExitWriteLock(); }
}
//....
void Provider_PositionChanged(object sender, SpecialEventArgs e)
{
   if (_updateLock.TryEnterWriteLock(0)) //noone is currently executing an update?
   {
       try { myUiControl.BeginInvoke(/*...*/); }
       finally { _updateLock.ExitWriteLock(); }               
   }

This all works, but is there a more elegant solution? How to simply test, from the one thread, whether a method is executed on any (other) thread?

  • Note: Using Invoke() (instead of BeginInvoke()) is not an option, since this would block my background thread, preventing other stuff from executing there.
  • (1)It's MapXtreme, a mapping solution, and I want to pan/zoom large bitmap terrain data, plus updating some features.
  • PS. This question is somewhat related, but covers a different aspect: Winforms multithreading: Is creating a new delegate each time when invoking a method on the UI thread needed?

Thanks for any answers!

UPDATE: Hans Passant has helped me with his answer. See the solution below. Hopefully this helps someone else too.

/// <summary>
/// This class enqueues asynchronously executing actions (that are running on another thread), but allows
/// to execute only one action at a time. When busy, newly enqueued actions are dropped.
/// Any enqueued action is required to call Done() on this when it has finished, to allow further actions
/// to execute afterwards.
/// </summary>
/// <remarks>This class is intended to help prevent stacking UI-Updates when the CPU or other resources
/// on the machine are not able to handle the amount of updates requested. However, the user
/// must keep in mind, that using this class may result
/// in dropped updates and that the last requested update is not always executed.</remarks>
public class ActionBouncer
{
    /// <summary>
    /// A event that signals the idle/busy state. Idle means, that no action is currently executing.
    /// </summary>
    private ManualResetEvent _idle = new ManualResetEvent(true);

    /// <summary>
    /// Enqueues the specified action, executing it when this bouncer
    /// is currently idle.
    /// </summary>
    /// <param name="action">The action.</param>
    public void Enqueue(Action action)
    {
        if (_idle.WaitOne(0))  //are we idle now? (Remark: This check and the reset below is not thread-safe (thanks to s.skov))
        {
            _idle.Reset(); //go to busy state
            action(); //execute the action now.
        }//else drop the action immediately.
    }

    /// <summary>
    /// Signal the bouncer, that the currently executing asynchronous action is done, allowing 
    /// subsequent requests to execute.
    /// This must get explicitly called (in code) at the end of the asynchronous action. 
    /// </summary>
    public void Done()
    {
        _idle.Set();               
    }
}

回答1:


This code doesn't actually do what you want it to do. It takes a while for the delegate target to start running. Your worker thread could acquire the write lock many times before that. It will only fail to acquire the lock when the Update() method happens to be busy executing.

A ManualResetEvent is what you want here. Initialize it to be set. Reset() it when you BeginInvoke(), Set() it at the end of Update(). Now you can test with WaitOne(0).

Note a corner case with this approach: your UI may well not show the last update.




回答2:


Since you don't want to block the background thread you could use a simple non-blocking protector:

public void Update(double x, double y)
{   
    try
    { 
       //...long running task... 
    }
    finally
    { 
       Interlocked.CompareExchange(ref lockCookie, 0, 1);  //Reset to 0, if it is 1
    }
}
//....
void Provider_PositionChanged(object sender, SpecialEventArgs e)
{
    if (Interlocked.CompareExchange(ref lockCookie, 1, 0) == 0) //Set to 1, if it is 0
    {
        myUiControl.BeginInvoke(/*...*/);
    }       
}

This ensures that the BeginInvoke is only called after a completion of the Update method. Any subsequent 'attempts' will not enter the if..then block

EDIT: The same if..then can of course be used in both threads, as long as the lockCookie is the same and added finally as per commenter suggestion.




回答3:


My preferred approach is to define the display object in such a way that the underlying state may be updated asynchronously, so that the update command run on the UI thread doesn't need any parameters. I then have a flag which says whether an update is pending. After any change to the state, I Interlocked.Exchange the flag and, if no change was pending, I BeginInvoke the update routine. The UpdateRoutine, while the flag is set, clears the flag and does an update. If the state gets changed during an update, the update may or may not reflect the state change, but precisely one more update will happen following the last state change.

In some cases, it may be desirable to associate a timer with the update routine; initially the timer starts disabled. If an update request is received and the timer is enabled, do skip the update. Otherwise, perform the update and enable the timer (with e.g. a 50-ms interval). When the timer expires, if the update flag is set, perform another update. This approach will greatly reduce overhead if the main code attempts to e.g. update a progress bar 10,000x/second.



来源:https://stackoverflow.com/questions/4286708/winforms-multithreading-execute-a-gui-update-only-if-the-previous-one-has-finis

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!