WinForms多线程:仅在前一个更新完成后才执行GUI更新

时间:2010-11-26 15:52:40

标签: c# winforms multithreading asynchronous methods

我有一些带有后台处理的多线程应用程序。它具有长时间运行的UI更新(在UI线程本身上),它们通过众所周知的resource on MSDN从后台线程调用。我无法缩短这些UI更新,因为它们最终在外部库中完成(1)。

现在,从后台线程我想在UI线程上异步调用(使用BeginInvoke())这些更新,但前提是尚未完成上一次更新。如果没有,我想简单地跳过此更新。这将防止Windows消息队列溢出,以防调用比调用方法能够执行的速度快。

我目前的解决方案是: 在UI线程上执行的方法中,我进入并退出ReaderWriterLockSlim实例。在后台线程上,我尝试以零超时进入实例。成功后,我调用'BeginInvoke()'然后再次退出。如果不成功,我会完全跳过调用方法。

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

这一切都有效,但有更优雅的解决方案吗?如何从一个线程中简单地测试一个方法是否在任何(其他)线程上执行?

感谢您的回答!

更新 Hans Passant帮助我解答了他的问题。 请参阅下面的解决方案。希望这也有助于其他人。

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

3 个答案:

答案 0 :(得分:2)

由于您不想阻止后台线程,因此您可以使用简单的非阻塞保护程序:

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(/*...*/);
    }       
}

这可确保仅在完成BeginInvoke方法后调用Update。任何后续“尝试”都不会进入if..then

编辑:当然if..then可以在两个帖子中使用,只要lockCookie是相同的,并根据评论者的建议最终添加。

答案 1 :(得分:2)

此代码实际上并不是您希望它执行的操作。委托目标开始运行需要一段时间。在此之前,您的工作线程可能会多次获取写锁定。当Update()方法正忙于执行时,它将无法获取锁。

您想要的是ManualResetEvent。初始化它以进行设置。在BeginInvoke()时重置()它,在Update()结束时设置()它。现在您可以使用WaitOne(0)进行测试。

请注意使用此方法的极端情况:您的用户界面可能显示上次更新。

答案 2 :(得分:0)

我首选的方法是以这样一种方式定义显示对象,即可以异步更新基础状态,以便在UI线程上运行的更新命令不需要任何参数。然后我有一个标志,说明更新是否正在等待。在对状态进行任何更改后,我Interlocked.Exchange标志,如果没有挂起的更改,我BeginInvoke更新例程。设置标志时,UpdateRoutine清除标志并进行更新。如果状态在更新期间发生更改,则更新可能会也可能不会反映状态更改,但在上次状态更改后,将会再发生一次更新。

在某些情况下,可能需要将计时器与更新例程相关联;最初计时器开始禁用。如果收到更新请求并且启用了计时器,请跳过更新。否则,执行更新并启用计时器(例如,间隔为50毫秒)。当计时器到期时,如果设置了更新标志,则执行另一次更新。如果主代码尝试例如,这种方法将大大减少开销。更新进度条10,000x /秒。