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

3
我有一个多线程应用程序,其中包含一些后台处理。它具有长时间运行的UI更新(在UI线程本身上),这些更新通过众所周知的MSDN资源从后台线程调用。由于最终在外部库(1)中完成这些UI更新,因此我无法缩短它们。
现在,我想要在后台线程中异步地使用BeginInvoke()调用这些更新到UI线程上,但仅在先前的更新已经完成时才进行。如果没有完成,我希望简单地跳过此更新。这将防止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(); }               
   }

这个方法可以工作,但是否有更优雅的解决方案呢?如何仅从一个线程简单地测试某个方法是否在任何(其他)线程上执行?
注意:使用Invoke()(而不是BeginInvoke())不是选项,因为这会阻止我的后台线程,从而防止其他东西在那里执行。
(1)这是一个地图解决方案MapXtreme,我想平移/缩放大型位图地形数据,并更新一些特性。
PS。这个问题有点相关,但涵盖了不同的方面:Winforms multithreading: Is creating a new delegate each time when invoking a method on the UI thread needed? 感谢任何答案!
更新: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();               
    }
}

1
但是,除非您锁定对Enqueue的访问,否则它不是线程安全的。在_idle.WaitOne和_idle.Reset之间可以有多个线程运行。这似乎对您的情况没有影响,但如果有多个工作线程调用Enqueue,则值得知道-也许更新代码注释以反映此情况。 - S.Skov
@S.Skov:你对Enqueue()的线程安全性的评论是正确的。正如你猜测的那样,这对我的应用程序不是问题,因为工作线程始终是相同的。然而,这个问题应该得到解决! - Marcel
3个回答

2
这段代码实际上并不能完成你想要的功能。在委托目标开始运行之前需要花费一些时间。在此期间,你的工作线程可能会多次获取写锁。只有当Update()方法恰好在执行时,它才会无法获得锁。在这种情况下,您可以使用ManualResetEvent。将其初始化为设置状态,在BeginInvoke()时重置,Update()结束时设置。现在可以使用WaitOne(0)进行测试。请注意,这种方法存在一个特例:您的UI可能不会显示最后一次更新。

谢谢Hans,给了我正确的答案,并且真正理解了我的意图。现在我已经创建了一个类来封装你的想法,并与问题分享。 - Marcel

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

这确保只有在“Update”方法完成后才调用“BeginInvoke”。任何后续的“尝试”都不会进入“if..then”块。
编辑:同样的“if..then”当然可以在两个线程中使用,只要“lockCookie”相同并根据评论者的建议最终添加即可。

你认为你没有使用锁吗?你使用的方式不太好,而不是使用lock,你自己实现了它,locktrycatchfinally语句和Monitor,而你的方式没有。 - Saeed Amiri
额,什么?我没有在任何地方使用锁。如果你在考虑lockCookie,那么它是在某个范围内声明的数字变量。Interlocked**确保原子性,但不会将给定线程置于阻塞模式。 - S.Skov
我认为你使用了 while,也许我无法理解 OP 的问题,但是你的方法存在一些未使用的函数调用。 - Saeed Amiri
他的主线程通过BeginInvoke调用进行了一些更新。在此过程中,他不想启动另一个更新,但他也不能阻塞启动BeginInvoke的线程。因此,他不能使用锁定或任何其他类似的阻塞同步选项 - 是的,你是对的,在我的示例中确实缺少finally。 - S.Skov

0

我倾向于采用一种方法,即以这样的方式定义显示对象,以便底层状态可以异步更新,这样在UI线程上运行的更新命令就不需要任何参数。然后我有一个标志,表示是否有待处理的更新。在状态发生任何变化后,我使用Interlocked.Exchange来交换标志,并且如果没有待处理的变化,我会调用BeginInvoke来执行更新程序。当标志处于设置状态时,UpdateRoutine将清除标志并进行更新。如果在更新期间更改了状态,则更新可能或可能不反映状态更改,但是在最后一次状态更改之后,将会再次进行精确的一次更新。

在某些情况下,可能希望将定时器与更新程序关联起来;初始情况下,定时器处于禁用状态。如果接收到更新请求并且定时器已启用,请跳过更新。否则,执行更新并启用定时器(例如,使用50毫秒的间隔)。当定时器到期时,如果更新标志已设置,则执行另一次更新。如果主代码尝试每秒更新进度条10,000次,这种方法将极大地减少开销。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接