【问题标题】:WinForms Multithreading: Execute a GUI update only if the previous one has finishedWinForms 多线程:仅在前一个已完成时执行 GUI 更新
【发布时间】:2011-05-16 06:16:59
【问题描述】:

我有带有一些后台处理的多线程应用程序。它具有长时间运行的 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();               
    }
}

【问题讨论】:

  • 但它不是线程安全的,除非您锁定对 Enqueue 的访问。 _idle.WaitOne 和 _idle.Reset 之间可以有多个线程。对于您的情况,这似乎不是问题,但如果您有多个工作线程调用 Enqueue,则值得了解 - 可能更新代码注释以反映这一点。
  • @S.Skov:您对 Enqueue() 线程安全的评论是正确的。正如您猜对的那样,这对我的应用程序来说不是问题,因为工作线程总是相同的。但是,这应该得到解决!

标签: c# winforms multithreading asynchronous methods


【解决方案1】:

这段代码实际上并没有做你想做的事。委托目标开始运行需要一段时间。在此之前,您的工作线程可以多次获取写锁。只有在Update()方法正忙于执行时才会获取锁失败。

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

请注意这种方法的一个极端情况:您的 UI 很可能显示上次更新。

【讨论】:

  • 感谢汉斯,他给了我正确的答案,并真正理解了我的意图。我现在创建了一个类来包装您的想法,并与问题分享。
【解决方案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 相同并最终根据评论者的建议添加。

【讨论】:

  • 你认为你没有使用锁定?你以一种不好的方式使用它,而不是使用lock 你自己实现它,lock 有trycatchfinally 语句和Monitor,而你的方式没有它.
  • 呃什么?我不在任何地方使用锁。如果您正在考虑 lockCookie,那么它是在范围内某处声明的数字变量。 Interlocked** 确保原子性,但不会将给定线程置于阻塞模式。
  • 我认为你使用了while,可能是我无法理解OPs 的问题,但是你这样做的方式,有一些未使用的函数调用。
  • 他的主线程通过 BeginInvoke 调用做了一些更新。虽然这样他不想开始另一个更新,但他也不能阻塞启动 BeginInvoke 的线程。所以他不能使用锁或任何其他类似的阻塞同步选项 - 是的,你是对的,我的示例中最终丢失了。
【解决方案3】:

我的首选方法是定义显示对象,使底层状态可以异步更新,以便在 UI 线程上运行的更新命令不需要任何参数。然后我有一个标志,说明是否有更新待处理。在对状态进行任何更改后,我 Interlocked.Exchange 标志,如果没有未决的更改,我 BeginInvoke 更新例程。 UpdateRoutine 在设置标志时清除标志并进行更新。如果在更新期间状态发生变化,则更新可能会也可能不会反映状态变化,但恰好在最后一次状态变化之后会再发生一次更新。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-10-17
    • 1970-01-01
    • 1970-01-01
    • 2016-10-11
    • 2011-06-23
    相关资源
    最近更新 更多