Мертвая блокировка в WinForms, который предотвращен щелчком правой кнопкой по панели задач

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

Теперь UI приложения имеет кнопки, чтобы "Запустить" и "Остановить" сетевой механизм. Обе работы кнопок. Для создания моего компонента ориентированным на многопотоковое исполнение, я использую блокировку приблизительно три метода. Я, которого dont't хотят, чтобы клиент смог назвать Остановкой () прежде, Запускаю () законченный. Additinally там является Таймером Опроса.

Я пытался показать Вас как можно меньше строк и simpified проблема:

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 (), и во время казни TimerCallback нажимают Stop (), это предотвращает TimerCallback для возврата. Это зависает точно в том же положении, StatusEvent. Таким образом, блокировка никогда не выпускается, и GUI также зависает, потому что это - вызов Остановки (), метод не может продолжиться.

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

Между прочим, я также попробовал его InvokeIfRequired, поскольку я не знаю стажеров приложения GUI. Это - neccesary, если мой 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. Таким образом, главное приложение ответственно за Вызов? Но так или иначе, AFAIK, не используя Вызывают, хотя необходимый не привел бы к мертвой блокировке как это, но (надо надеяться) в исключении.

5
задан mattytommo 19 March 2013 в 16:41
поделиться

6 ответов

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

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

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

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

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

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

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

[ update ]

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

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 ]

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

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

        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 ]

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

        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;
8
ответ дан 18 December 2019 в 14:47
поделиться

A couple things come to mind when reviewing your code. The first thing is that you are not checking for a null delegate before firing the status event. If no listeners are bound to the event, then this will cause an exception, which if not caught or handled, might cause strange issues in threaded code.

So the first thing I'd so is this:

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

The other thing that comes to mind is that perhaps your lock is failing you in some manner. What type of object are you using for the lock? The simplest thing to use is just a plain ole "object", but you must ensure you are not using a value type (e.g. int, float, etc.) that would be boxed for locking, thus never really establishing a lock since each lock statement would box and create a new object instance. You should also keep in mind that a lock only keeps "other" threads out. If called on the same thread, then it will sail through the lock statement.

2
ответ дан 18 December 2019 в 14:47
поделиться

If you don't have the source for the GUI (which you probably should) you can use Reflector to disassemble it. There is even a plugin to generate source files so you could run the app in your VS IDE and set breakpoints.

1
ответ дан 18 December 2019 в 14:47
поделиться

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

Кроме того, вам будет хорошо отключить "

1
ответ дан 18 December 2019 в 14:47
поделиться

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

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

0
ответ дан 18 December 2019 в 14:47
поделиться

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

Два способа снять блокировку:

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

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

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

1
ответ дан 18 December 2019 в 14:47
поделиться
Другие вопросы по тегам:

Похожие вопросы: