Дедлок в WinForms, который предотвращается щелчком правой кнопкой мыши на панели задач - PullRequest
5 голосов
/ 17 декабря 2009

Я столкнулся со странной проблемой с нашим приложением Windows C # / .NET. На самом деле это приложение с графическим интерфейсом, моя работа - это сетевой компонент, включенный в сборку. Я не знаю код основного приложения / приложения с графическим интерфейсом , я мог бы связаться с его разработчиком.

Теперь в пользовательском интерфейсе приложения есть кнопки «Пуск» и «Стоп» сетевого движка. Обе кнопки работают. Чтобы сделать мой компонент безопасным для потоков, я использую блокировку вокруг трех методов. Я не хочу, чтобы клиент мог вызывать Stop () до завершения Start (). Кроме того, есть таймер опроса.

Я попытался показать вам как можно меньше строк и упростил задачу:

private Timer actionTimer = new Timer(new
                TimerCallback(actionTimer_TimerCallback),
                null, Timeout.Infinite, Timeout.Infinite);

public void Start()
{
 lock (driverLock)
 {
  active = true;
  // Trigger the first timer event in 500ms
  actionTimer.Change(500, Timeout.Infinite);
 }
}

private void actionTimer_TimerCallback(object state)
{
 lock (driverLock)
 {
  if (!active) return;
  log.Debug("Before event");
  StatusEvent(this, new StatusEventArgs()); // it hangs here
  log.Debug("After event");
  // Now restart timer
  actionTimer.Change(500, Timeout.Infinite);
 }
}

public void Stop()
{
 lock (driverLock)
 {
  active = false;
 }
}

Вот как воспроизвести мою проблему. Как я уже сказал, обе кнопки Start и Stop работают, но если вы нажмете Start (), и во время выполнения TimerCallback нажмите Stop () , это не позволит вернуть TimerCallback. Он висит точно в той же позиции, StatusEvent. Таким образом, блокировка никогда не снимается, и графический интерфейс также зависает, потому что вызов метода Stop () не может быть продолжен.

Теперь я заметил следующее: если приложение зависает из-за этого «тупика», и я щелкаю приложение на панели задач правой кнопкой мыши, оно продолжается. Это просто работает, как и ожидалось. У кого-нибудь есть объяснение или лучшее решение для этого?

Кстати, я тоже попробовал это с InvokeIfRequired, так как я не знаю, как работает приложение GUI. Это необходимо, если мой StatusEvent что-то изменит в GUI. Поскольку у меня нет ссылок на элементы управления GUI, я использовал (предполагая только одну цель):

Delegate firstTarget = StatusEvent.GetInocationList()[0];
ISynchronizeInvoke syncInvoke = firstTarget.Target as ISynchronizeInvoke;
if (syncInvoke.InvokeRequired)
{
  syncInvoke.Invoke(firstTarget, new object[] { this, new StatusEventArgs() });
}
else
{
  firstTarget.Method.Invoke(firstTarget.Target, new object[] { this, new StatusEventArgs() });
}

Этот подход не изменил проблему. Я думаю, это потому, что я вызываю обработчики событий основного приложения, а не элементы управления GUI. То есть основное приложение отвечает за Invoking? Но в любом случае AFAIK, не использующий Invoke, хотя и необходимый, не приведет к такому тупику, но (надеюсь) к исключению.

Ответы [ 6 ]

8 голосов
/ 17 декабря 2009

Что касается того, почему щелчок правой кнопкой мыши «разблокирует» ваше приложение, мое «обоснованное предположение» о событиях, которые приводят к такому поведению, выглядит следующим образом:

  1. (когда ваш компонент был создан) GUI зарегистрировал подписчика на событие уведомления о состоянии
  2. Ваш компонент получает блокировку (в рабочем потоке, не поток GUI), а затем запускает событие уведомления о состоянии
  3. Вызывается обратный вызов GUI для события уведомления о статусе и начинается обновление GUI; обновления вызывают отправку событий в цикл обработки событий
  4. Пока идет обновление, нажимается кнопка «Пуск»
  5. Win32 отправляет сообщение о клике в поток графического интерфейса и пытается обработать его синхронно
  6. Вызывается обработчик для кнопки «Пуск», затем он вызывает метод «Пуск» для вашего компонента (в потоке GUI)
  7. Обратите внимание, что обновление статуса еще не завершено; кнопка запуска обработчика "вырезать перед" оставшиеся обновления графического интерфейса в обновлении статуса (на самом деле это происходит довольно редко в Win32)
  8. Метод «Пуск» пытается получить блокировку вашего компонента (в потоке GUI), блоки
  9. Поток GUI теперь зависает (ожидает завершения обработчика запуска; обработчик запуска ожидает блокировки; блокировка удерживается рабочим потоком, который направил вызов обновления GUI в поток GUI и ожидает завершения вызова обновления; обновление GUI вызов, маршалированный из рабочего потока, ожидает завершения обработчиком запуска, который обрезается перед ним; ...)
  10. Если вы теперь щелкните правой кнопкой мыши на панели задач, мое предположение состоит в том, что менеджер панели задач (каким-то образом) запускает "цикл суб-событий" (так же, как модальные диалоги запускают свои собственные "циклы суб-событий) ", смотрите подробности в блоге Раймонда Чена) и обрабатывает события в очереди для приложения
  11. Дополнительный цикл событий, запускаемый по щелчку правой кнопкой мыши, теперь может обрабатывать обновления графического интерфейса, которые были собраны из рабочего потока; это разблокирует рабочий поток; это в свою очередь снимает блокировку; это, в свою очередь, разблокирует поток графического интерфейса приложения, чтобы он мог завершить обработку нажатия кнопки запуска (поскольку теперь он может получить блокировку)

Вы можете проверить эту теорию, заставив ваше приложение «кусаться», а затем проникнуть в отладчик и посмотреть на трассировку стека рабочего потока для вашего компонента. Он должен быть заблокирован при переходе к потоку GUI. Сам поток GUI должен быть заблокирован в операторе блокировки, но вниз по стеку вы должны увидеть некоторые вызовы «вырезать перед строкой» ...

Я думаю, что первой рекомендацией, которая сможет отследить эту проблему, было бы включить флаг Control.CheckForIllegalCrossThreadCalls = true;.

Далее я бы порекомендовал запустить событие уведомления вне блокировки. Обычно я собираю информацию, необходимую для события внутри блокировки, затем снимаю блокировку и использую собранную информацию для запуска события. Нечто подобное:

string status;
lock (driverLock) {
    if (!active) { return; }
    status = ...
    actionTimer.Change(500, Timeout.Infinite);
}
StatusEvent(this, new StatusEventArgs(status));

Но самое главное, я бы рассмотрел, кто является предполагаемыми клиентами вашего компонента. Из названий методов и вашего описания я подозреваю, что GUI - единственный (он говорит вам, когда начинать и останавливаться; вы сообщаете ему, когда меняется ваш статус). В этом случае вы должны не использовать блокировку. Методы запуска и остановки могут просто устанавливать и сбрасывать событие ручного сброса, чтобы указать, активен ли ваш компонент (на самом деле семафор).

[ обновление ]

Пытаясь воспроизвести ваш сценарий, я написал следующую простую программу. Вы должны быть в состоянии скопировать код, скомпилировать и запустить его без проблем (я создал его как консольное приложение, которое запускает форму :-))

using System;
using System.Threading;
using System.Windows.Forms;

using Timer=System.Threading.Timer;

namespace LockTest
{
    public static class Program
    {
        // Used by component's notification event
        private sealed class MyEventArgs : EventArgs
        {
            public string NotificationText { get; set; }
        }

        // Simple component implementation; fires notification event 500 msecs after previous notification event finished
        private sealed class MyComponent
        {
            public MyComponent()
            {
                this._timer = new Timer(this.Notify, null, -1, -1); // not started yet
            }

            public void Start()
            {
                lock (this._lock)
                {
                    if (!this._active)
                    {
                        this._active = true;
                        this._timer.Change(TimeSpan.FromMilliseconds(500d), TimeSpan.FromMilliseconds(-1d));
                    }
                }
            }

            public void Stop()
            {
                lock (this._lock)
                {
                    this._active = false;
                }
            }

            public event EventHandler<MyEventArgs> Notification;

            private void Notify(object ignore) // this will be invoked invoked in the context of a threadpool worker thread
            {
                lock (this._lock)
                {
                    if (!this._active) { return; }
                    var notification = this.Notification; // make a local copy
                    if (notification != null)
                    {
                        notification(this, new MyEventArgs { NotificationText = "Now is " + DateTime.Now.ToString("o") });
                    }
                    this._timer.Change(TimeSpan.FromMilliseconds(500d), TimeSpan.FromMilliseconds(-1d)); // rinse and repeat
                }
            }

            private bool _active;
            private readonly object _lock = new object();
            private readonly Timer _timer;
        }

        // Simple form to excercise our component
        private sealed class MyForm : Form
        {
            public MyForm()
            {
                this.Text = "UI Lock Demo";
                this.AutoSize = true;
                this.AutoSizeMode = AutoSizeMode.GrowAndShrink;

                var container = new FlowLayoutPanel { FlowDirection = FlowDirection.TopDown, Dock = DockStyle.Fill, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink };
                this.Controls.Add(container);
                this._status = new Label { Width = 300, Text = "Ready, press Start" };
                container.Controls.Add(this._status);
                this._component.Notification += this.UpdateStatus;
                var button = new Button { Text = "Start" };
                button.Click += (sender, args) => this._component.Start();
                container.Controls.Add(button);
                button = new Button { Text = "Stop" };
                button.Click += (sender, args) => this._component.Stop();
                container.Controls.Add(button);
            }

            private void UpdateStatus(object sender, MyEventArgs args)
            {
                if (this.InvokeRequired)
                {
                    Thread.Sleep(2000);
                    this.Invoke(new EventHandler<MyEventArgs>(this.UpdateStatus), sender, args);
                }
                else
                {
                    this._status.Text = args.NotificationText;
                }
            }

            private readonly Label _status;
            private readonly MyComponent _component = new MyComponent();
        }

        // Program entry point, runs event loop for the form that excercises out component
        public static void Main(string[] args)
        {
            Control.CheckForIllegalCrossThreadCalls = true;
            Application.EnableVisualStyles();
            using (var form = new MyForm())
            {
                Application.Run(form);
            }
        }
    }
}

Как видите, код состоит из 3 частей: во-первых, компонент, использующий таймер для вызова метода уведомления каждые 500 миллисекунд; во-вторых, простая форма с кнопками метки и запуска / остановки; и, наконец, основная функция для запуска четного цикла.

Вы можете заблокировать приложение, нажав кнопку «Пуск», а затем в течение 2 секунд нажав кнопку «Стоп». Однако приложение не «размораживается», когда я щелкаю правой кнопкой мыши на панели задач, вздох.

Когда я врываюсь в заблокированное приложение, это то, что я вижу, когда переключаюсь на рабочий поток (таймер):

Worker thread

И вот что я вижу при переключении на основной поток:

Main thread

Буду признателен, если вы попробуете скомпилировать и запустить этот пример; если он работает для вас так же, как и я, вы можете попытаться обновить код, чтобы он был более похож на то, что есть в вашем приложении, и, возможно, мы сможем воспроизвести вашу точную проблему. Как только мы воспроизведем его в таком тестовом приложении, не должно возникнуть проблем с его рефакторингом, чтобы устранить проблему (мы бы изолировали суть проблемы).

[ обновление 2 ]

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

При этом я хотел бы дать следующую рекомендацию. Не могли бы вы попробовать эти изменения в вашем приложении и сообщить мне, решили ли они проблему взаимоблокировки? По сути, вы бы переместили ВСЕ код компонента в рабочие потоки (то есть ничего, что не имеет отношения к вашему компоненту, больше не будет работать в потоке GUI, кроме кода для делегирования рабочим потокам :-)) ...

        public void Start()
        {
            ThreadPool.QueueUserWorkItem(delegate // added
            {
                lock (this._lock)
                {
                    if (!this._active)
                    {
                        this._active = true;
                        this._timer.Change(TimeSpan.FromMilliseconds(500d), TimeSpan.FromMilliseconds(-1d));
                    }
                }
            });
        }

        public void Stop()
        {
            ThreadPool.QueueUserWorkItem(delegate // added
            {
                lock (this._lock)
                {
                    this._active = false;
                }
            });
        }

Я переместил тело методов Start и Stop в рабочий поток пула потоков (так же, как ваши таймеры регулярно вызывают ваш обратный вызов в контексте работника пула потоков). Это означает, что поток GUI никогда не будет владеть блокировкой, блокировка будет получена только в контексте (возможно, различного для каждого вызова) рабочих потоков пула потоков.

Обратите внимание, что с указанным выше изменением моя программа-пример больше не блокируется (даже с «Invoke» вместо «BeginInvoke»).

[ обновление 3 ]

Согласно вашему комментарию, метод Start из очереди недопустим, поскольку он должен указывать, был ли компонент запущен. В этом случае я бы рекомендовал по-разному относиться к «активному» флагу. Вы должны переключиться на «int» (0 остановлено, 1 запущено) и использовать статические методы «Interlocked» для манипулирования им (я предполагаю, что ваш компонент имеет большее состояние, которое он предоставляет - вы бы защищали доступ к чему-либо, кроме флага «active» с вашим замок):

        public bool Start()
        {
            if (0 == Interlocked.CompareExchange(ref this._active, 0, 0)) // will evaluate to true if we're not started; this is a variation on the double-checked locking pattern, without the problems associated with lack of memory barriers (see http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html)
            {
                lock (this._lock) // serialize all Start calls that are invoked on an un-started component from different threads
                {
                    if (this._active == 0) // make sure only the first Start call gets through to actual start, 2nd part of double-checked locking pattern
                    {
                        // run component startup

                        this._timer.Change(TimeSpan.FromMilliseconds(500d), TimeSpan.FromMilliseconds(-1d));
                        Interlocked.Exchange(ref this._active, 1); // now mark the component as successfully started
                    }
                }
            }
            return true;
        }

        public void Stop()
        {
            Interlocked.Exchange(ref this._active, 0);
        }

        private void Notify(object ignore) // this will be invoked invoked in the context of a threadpool worker thread
        {
            if (0 != Interlocked.CompareExchange(ref this._active, 0, 0)) // only handle the timer event in started components (notice the pattern is the same as in Start method except for the return value comparison)
            {
                lock (this._lock) // protect internal state
                {
                    if (this._active != 0)
                    {
                        var notification = this.Notification; // make a local copy
                        if (notification != null)
                        {
                            notification(this, new MyEventArgs { NotificationText = "Now is " + DateTime.Now.ToString("o") });
                        }
                        this._timer.Change(TimeSpan.FromMilliseconds(500d), TimeSpan.FromMilliseconds(-1d)); // rinse and repeat
                    }
                }
            }
        }

        private int _active;
2 голосов
/ 17 декабря 2009

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

Итак, первое, что я бы сказал, так это:

if(StatusEvent != null)
{
  StatusEvent(this, new StatusEventArgs());
}

Еще одна вещь, которая приходит на ум, это то, что, возможно, ваш замок каким-то образом подведет вас. Какой тип объекта вы используете для блокировки? Простейшая вещь, которую нужно использовать, - это простой ole-объект, но вы должны убедиться, что вы не используете тип значения (например, int, float и т. Д.), Который был бы упакован для блокировки, таким образом, никогда не устанавливая блокировку, так как каждая блокировка оператор будет боксировать и создавать новый экземпляр объекта. Вы также должны помнить, что блокировка не позволяет «другим» потокам выходить. Если вызывается в том же потоке, он пропустит оператор блокировки.

1 голос
/ 17 декабря 2009

Да, это классический тупиковый сценарий. StatusEvent не может продолжаться, потому что ему необходим поток пользовательского интерфейса для обновления элементов управления. Тем не менее поток пользовательского интерфейса застрял, пытаясь получить driverLock. Удерживается кодом, который вызывает StatusEvent. Ни один поток не может продолжить.

Два способа взломать замок:

  • Код StatusEvent не обязательно должен выполняться синхронно. Используйте BeginInvoke вместо Invoke.
  • потоку пользовательского интерфейса не обязательно нужно ждать остановки потока. Ваша тема может уведомить об этом позже.

В ваших фрагментах недостаточно контекста, чтобы решить, какой из них лучше.

Обратите внимание, что у вас может быть потенциальная гонка по таймеру, она не видна в вашем фрагменте. Но обратный вызов может выполняться микросекунды после остановки таймера. Избегайте такого рода головной боли, используя реальный поток вместо обратного вызова таймера. Он может делать что-то периодически, вызывая WaitOne () для ManualResetEvent, передавая значение времени ожидания. Этот ManualResetEvent хорош для подачи сигнала на остановку.

1 голос
/ 17 декабря 2009

Отсутствие доступа к источнику графического интерфейса делает это сложнее, но общий совет здесь ... Графический интерфейс WinForm не является управляемым кодом и плохо сочетается с потоками .NET. Рекомендуемое решение для этого - использовать BackgroundWorker для порождения потока, независимого от WinForm. После запуска в потоке, запущенном BackgroundWorker, вы работаете в чистом управляемом коде и можете использовать таймеры .NET и многопоточность практически для чего угодно. Ограничение состоит в том, что вы должны использовать события BackgroundWorker для передачи информации обратно в графический интерфейс, и ваш поток, запущенный BackgroundWorker, не может получить доступ к элементам управления Winform.

Кроме того, вы можете отключить кнопку «Стоп» во время выполнения задачи «Пуск», и наоборот. Но BackgroundWorker - все еще путь; таким образом WinForm не зависает во время работы фонового потока.

1 голос
/ 17 декабря 2009

Если у вас нет источника для GUI (который вам, вероятно, следует), вы можете использовать Reflector , чтобы разобрать его. Существует даже плагин для генерации исходных файлов, чтобы вы могли запускать приложение в VS IDE и устанавливать точки останова.

0 голосов
/ 17 декабря 2009

Дикая догадка: может ли сообщение о состоянии каким-то образом вызвать другое приложение для вызова вашей задачи Stop?

Я бы поставил отладочную информацию в начало всех трех методов, посмотрите, не зашли ли вы на себя.

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