«Тупик» только с одним заблокированным объектом? - PullRequest
6 голосов
/ 28 января 2012

У меня проблема с многопоточностью в C #. Я использую событие для обновления метки в форме из другого потока, для чего мне, конечно, нужно использовать команду Invoke (). Эта часть также работает нормально. Тем не менее, пользователь может закрыть форму, и здесь программа может произойти сбой, если событие отправлено в неудачное время.

Итак, я решил переопределить метод Dispose () формы, установить логическое значение true в заблокированном коде, а также проверить это логическое значение и вызвать событие в заблокированном коде.

Однако при каждом закрытии формы программа полностью зависает.

Вот упомянутые части кода:

private object dispose_lock = new object();
private bool _disposed = false;

private void update(object sender, EventArgs e)
{
    if (InvokeRequired)
    {
        EventHandler handler = new EventHandler(update);
        lock (dispose_lock)
        {
            if (_disposed) return;
            Invoke(handler); // this is where it crashes without using the lock
        }
        return;
    }

    label.Text = "blah";
}

protected override void Dispose(bool disposing)
{
    eventfullObject.OnUpdate -= update;
    lock (dispose_lock) // this is where it seems to freeze
    {
       _disposed = true; // this is never called
    }
    base.Dispose(disposing);
}

Я надеюсь, что кто-нибудь здесь знает, что не так с этим кодом. Заранее спасибо!

Ответы [ 7 ]

6 голосов
/ 28 января 2012

То, что вы не учитываете, - то, что делегат, переданный Invoke, вызывается асинхронно в потоке пользовательского интерфейса.Вызов Invoke отправляет сообщение в очередь сообщений форм и через некоторое время принимается.

Что происходит не так:

UI Thread                   Background Thread
                            Call update()
                            take lock
                            Call Invoke()
Call update()             
                            release lock
Call Dispose()
take lock
release lock

А вместо:

UI Thread                   Background Thread
                            Call update()
                              take lock
                               Call Invoke()
                               block until UI Thread processes the message
Process messages
...
Dispose() 
   wait for lock ****** Deadlock! *****
...
Call update()             
                            release lock

Из-за этого фоновый поток может удерживать блокировку, пока поток пользовательского интерфейса пытается запустить Dispose

Решение гораздо проще, чем вы пытались.Поскольку Invoke публикуется асинхронно, блокировка не требуется.

private bool _disposed = false;

private void update(object sender, EventArgs e)
{
    if (InvokeRequired)
    {
        EventHandler handler = new EventHandler(update);
        Invoke(handler);
        return;
    }

    if (_disposed) return;

    label.Text = "blah";
}

protected override void Dispose(bool disposing)
{
    eventfullObject.OnUpdate -= update;
    _disposed = true; // this is never called
    base.Dispose(disposing);
}

Флаг _disposed только читается или записывается в потоке пользовательского интерфейса, поэтому блокировка не требуется.Теперь вызов стека выглядит так:

UI Thread                   Background Thread
                            Call update()
                               take lock
                               Call Invoke()
                               block until UI Thread processes the message
Process messages
...
Dispose() 
  _disposed = true;
...

Call update()
  _disposed is true so do nothing             
1 голос
/ 18 марта 2015

Другой сценарий взаимоблокировки возникает при вызове Dispatcher.Invoke (в приложении WPF) или Control.Invoke (в приложении Windows Forms) при наличии блокировки.Если пользовательский интерфейс запускает другой метод, ожидающий той же блокировки, тут же произойдет тупик.Это часто можно исправить, просто вызвав BeginInvoke вместо Invoke.Кроме того, вы можете снять блокировку перед вызовом Invoke, хотя это не сработает, если ваш абонент снял блокировку.Мы объясняем Invoke и BeginInvoke в приложениях Rich Client и Thread Affinity.

source: http://www.albahari.com/threading/part2.aspx

1 голос
/ 28 января 2012

Почему бы вам просто не использовать BeginInvoke вместо Invoke - это не заблокирует фоновый поток. Не похоже, что есть какая-то конкретная причина, по которой фоновый поток должен ждать, пока произойдет обновление пользовательского интерфейса, из показанного вами

1 голос
/ 28 января 2012

Одна из опасностей использования Control.Invoke заключается в том, что он может быть размещен в потоке пользовательского интерфейса в неудачное время, как вы предложили. Наиболее распространенный способ это происходит, когда у вас есть следующий порядок событий

  1. Фоновая тема: Очередь обратного вызова с Invoke
  2. Тема переднего плана: Укажите элемент управления, на котором фон называется Invoke
  3. Тема переднего плана: отменяет обратный вызов на расположенном элементе управления

В этом случае Invoke завершится ошибкой и вызовет исключение в фоновом потоке. Скорее всего, именно это и привело к аварийному завершению работы вашего приложения.

С новым кодом, хотя это вызывает мертвую блокировку. Код выполнит блокировку на шаге № 1. Затем удаление происходит в пользовательском интерфейсе на шаге № 2, и он ожидает блокировки, которая не будет снята до завершения шага № 3.

Самый простой способ справиться с этой проблемой - это признать, что Invoke - это операция, которая может и не удастся, следовательно, требуется try / catch

private void update(object sender, EventArgs e)
{
    if (InvokeRequired)
    {
        EventHandler handler = new EventHandler(update);
        try 
        {
            Invoke(handler); 
        }
        catch (Exception) 
        {
            // Control disposed while invoking.  Nothing to do 
        }
        return;
    }

    label.Text = "blah";
}
1 голос
/ 28 января 2012

Я бы на самом деле пошел просто.Вместо того, чтобы реализовывать сложный потокобезопасный код, я просто перехватил бы исключение и ничего не делал бы, если бы он не работал.

Предполагая, что это ObjectDisposedException:

try
{
    this.Invoke(Invoke(handler));
}
catch (ObjectDisposedException)
{
    // Won't do anything here as
    // the object is not in the good state (diposed when closed)
    // so we can't invoke.
}

Это проще и довольно просто.Если в комментарии указано почему вы поймали исключение, я думаю, что все в порядке.

0 голосов
/ 28 января 2012

Просто если ни один из других ответов не является виновником, есть ли другой код, который завершает поток, который не опубликован? Я думаю, что вы можете использовать простые потоки, а не BackgroundWorker, и, возможно, забыли установить Thread.isBackround в true

0 голосов
/ 28 января 2012

ИМО Dispose слишком поздно ...

Я бы рекомендовал поместить некоторый код в FormClosing, который вызывается до того, как Dispose произойдет AFAIK.

В таком случае я обычно склоняюсь использовать другой (атомарный) шаблон для вашей проверки - например, через класс Interlocked.

private long _runnable = 1;

private void update(object sender, EventArgs e)
{
    if (InvokeRequired)
    {
        EventHandler handler = new EventHandler(update);
        if (Interlocked.Read (ref _runnable) == 1) Invoke(handler);
        return;
    }

    label.Text = "blah";
}

В FormClosing вы просто звоните Interlocked.Increment (ref _runnable).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...