Многопоточность WinForms: выполнять обновление графического интерфейса, только если предыдущее завершено - PullRequest
3 голосов
/ 26 ноября 2010

У меня многопоточное приложение с некоторой фоновой обработкой. Он имеет долгосрочные обновления пользовательского интерфейса (в самом потоке пользовательского интерфейса), которые вызываются из фонового потока через известный ресурс в MSDN . Я не могу сократить эти обновления пользовательского интерфейса, так как они, наконец, сделаны во внешней библиотеке (1).

Теперь из этого фонового потока Я хочу асинхронно вызывать (используя BeginInvoke()) эти обновления в потоке пользовательского интерфейса, но только если предыдущее обновление еще не завершено. Если нет, я бы хотел просто пропустить это обновление. Это предотвратит переполнение очереди сообщений Windows в случае, если вызовы выполняются быстрее, чем способ вызванного метода.

Мое текущее решение таково: В методе, который выполняется в потоке пользовательского интерфейса, я вхожу и выхожу из экземпляра 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(); }               
   }

Это все работает, но есть ли более элегантное решение? Как просто из одного потока проверить, выполняется ли метод в каком-либо (другом) потоке?

Спасибо за любые ответы!

UPDATE: Ханс Пассант помог мне с ответом. См. Решение ниже. Надеюсь, это поможет кому-то еще.

/// <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 ]

2 голосов
/ 26 ноября 2010

Этот код на самом деле не делает то, что вы хотите.Требуется некоторое время для запуска цели делегата.Ваш рабочий поток мог получить блокировку записи много раз до этого.Он не сможет получить блокировку только тогда, когда метод Update () занят выполнением.

ManualResetEvent - это то, что вам нужно.Инициализируйте это, чтобы быть установленным.Reset () это когда вы BeginInvoke (), Set () это в конце Update ().Теперь вы можете выполнить тестирование с помощью WaitOne (0).

Обратите внимание на такой случай с этим подходом: ваш пользовательский интерфейс может не показывать последнее обновление.

2 голосов
/ 26 ноября 2010

Поскольку вы не хотите блокировать фоновый поток, вы можете использовать простой неблокирующий протектор:

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 одинаково и добавлено окончательно согласнопредложение комментатора.

0 голосов
/ 23 декабря 2010

Мой предпочтительный подход заключается в определении экранного объекта таким образом, чтобы базовое состояние могло обновляться асинхронно, чтобы команда обновления, выполняемая в потоке пользовательского интерфейса, не нуждалась в каких-либо параметрах.Затем у меня есть флаг, который говорит, ожидает ли обновление.После любого изменения состояния я блокирую флаг. Измените флаг и, если никаких изменений не было, я начинаю вызывать процедуру обновления.UpdateRoutine, пока флаг установлен, очищает флаг и выполняет обновление.Если состояние изменяется во время обновления, обновление может отражать или не отражать изменение состояния, но точно еще одно обновление произойдет после последнего изменения состояния.

В некоторых случаях может быть желательно связатьтаймер с процедурой обновления;изначально таймер начинает отключаться.Если запрос на обновление получен и таймер включен, пропустите обновление.В противном случае выполните обновление и включите таймер (например, с интервалом 50 мс).По истечении времени таймера, если установлен флаг обновления, выполните другое обновление.Этот подход значительно сократит накладные расходы, если основной код попытается, например, обновить индикатор выполнения в 10 000 раз в секунду.

...