Что касается того, почему щелчок правой кнопкой мыши «разблокирует» ваше приложение, мое «обоснованное предположение» о событиях, которые приводят к такому поведению, выглядит следующим образом:
- (когда ваш компонент был создан) GUI зарегистрировал подписчика на событие уведомления о состоянии
- Ваш компонент получает блокировку (в рабочем потоке, не поток GUI), а затем запускает событие уведомления о состоянии
- Вызывается обратный вызов GUI для события уведомления о статусе и начинается обновление GUI; обновления вызывают отправку событий в цикл обработки событий
- Пока идет обновление, нажимается кнопка «Пуск»
- Win32 отправляет сообщение о клике в поток графического интерфейса и пытается обработать его синхронно
- Вызывается обработчик для кнопки «Пуск», затем он вызывает метод «Пуск» для вашего компонента (в потоке GUI)
- Обратите внимание, что обновление статуса еще не завершено; кнопка запуска обработчика "вырезать перед"
оставшиеся обновления графического интерфейса в обновлении статуса (на самом деле это происходит довольно редко в Win32)
- Метод «Пуск» пытается получить блокировку вашего компонента (в потоке GUI), блоки
- Поток GUI теперь зависает (ожидает завершения обработчика запуска; обработчик запуска ожидает блокировки; блокировка удерживается рабочим потоком, который направил вызов обновления GUI в поток GUI и ожидает завершения вызова обновления; обновление GUI вызов, маршалированный из рабочего потока, ожидает завершения обработчиком запуска, который обрезается перед ним; ...)
- Если вы теперь щелкните правой кнопкой мыши на панели задач, мое предположение состоит в том, что менеджер панели задач (каким-то образом) запускает "цикл суб-событий" (так же, как модальные диалоги запускают свои собственные "циклы суб-событий) ", смотрите подробности в блоге Раймонда Чена) и обрабатывает события в очереди для приложения
- Дополнительный цикл событий, запускаемый по щелчку правой кнопкой мыши, теперь может обрабатывать обновления графического интерфейса, которые были собраны из рабочего потока; это разблокирует рабочий поток; это в свою очередь снимает блокировку; это, в свою очередь, разблокирует поток графического интерфейса приложения, чтобы он мог завершить обработку нажатия кнопки запуска (поскольку теперь он может получить блокировку)
Вы можете проверить эту теорию, заставив ваше приложение «кусаться», а затем проникнуть в отладчик и посмотреть на трассировку стека рабочего потока для вашего компонента. Он должен быть заблокирован при переходе к потоку 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 секунд нажав кнопку «Стоп». Однако приложение не «размораживается», когда я щелкаю правой кнопкой мыши на панели задач, вздох.
Когда я врываюсь в заблокированное приложение, это то, что я вижу, когда переключаюсь на рабочий поток (таймер):
И вот что я вижу при переключении на основной поток:
Буду признателен, если вы попробуете скомпилировать и запустить этот пример; если он работает для вас так же, как и я, вы можете попытаться обновить код, чтобы он был более похож на то, что есть в вашем приложении, и, возможно, мы сможем воспроизвести вашу точную проблему. Как только мы воспроизведем его в таком тестовом приложении, не должно возникнуть проблем с его рефакторингом, чтобы устранить проблему (мы бы изолировали суть проблемы).
[ обновление 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;