Я встретился со странной проблемой с нашим 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, не используя Вызывают, хотя необходимый не привел бы к мертвой блокировке как это, но (надо надеяться) в исключении.
Что касается того, почему щелчок правой кнопкой мыши «разблокирует» ваше приложение, мое «обоснованное предположение» о событиях, которые приводят к такому поведению, выглядит следующим образом:
. Вы можете проверить эту теорию, заставив ваше приложение "кусаться", затем взломав отладчик и посмотрев на стек трассировка рабочего потока для вашего компонента. Он должен быть заблокирован при переходе в поток графического интерфейса. Сам поток графического интерфейса пользователя должен быть заблокирован в операторе блокировки, но ниже по стеку вы должны увидеть некоторые вызовы «вырезать перед строкой» ...
Я думаю, что первая рекомендация, позволяющая отследить эту проблему, - включить флаг 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 секунд нажав кнопку остановки. Однако приложение не «размораживается», когда я щелкаю правой кнопкой мыши на панели задач, вздох.
Когда я взламываю заблокированное приложение, при переключении на рабочий поток (таймер) я вижу следующее:
И вот что я вижу при переключении на основной поток:
Я был бы признателен, если бы вы могли попробовать скомпилировать и запустить этот пример; если он работает для вас так же, как и у меня, вы можете попробовать обновить код, чтобы он был более похож на тот, который есть в вашем приложении, и, возможно, мы сможем воспроизвести вашу точную проблему. После того, как мы воспроизведем его в тестовом приложении, подобном этому, не должно возникнуть проблем с его рефакторингом, чтобы проблема исчезла (мы бы изолировали суть проблемы).
[ обновление 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;
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.
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.
Отсутствие доступа к источнику графического интерфейса делает это сложнее, но общий совет ... Графический интерфейс WinForm не является управляемым кодом и плохо сочетается с потоками .NET. Для этого рекомендуется использовать BackgroundWorker для создания потока, независимого от WinForm. Когда вы работаете в потоке, запущенном BackgroundWorker, вы находитесь в чистом управляемом коде и можете использовать таймеры и потоки .NET практически для чего угодно. Ограничение заключается в том, что вы должны использовать события BackgroundWorker для передачи информации обратно в графический интерфейс, а ваш поток, запущенный BackgroundWorker, не может получить доступ к элементам управления Winform.
Кроме того, вам будет хорошо отключить "
Здесь дикая догадка: могло ли сообщение о статусе каким-то образом заставлять другое приложение вызывать вашу задачу Stop?
Я бы поставил отладочную информацию в начале всех трех методов, посмотрите, сможете ли вы заходите в тупик на себе.
Да, это классический сценарий взаимоблокировки. StatusEvent не может продолжаться, потому что ему нужен поток пользовательского интерфейса для обновления элементов управления. Однако поток пользовательского интерфейса зависает, пытаясь получить driverLock. Удерживается кодом, вызывающим StatusEvent. Ни один из потоков не может продолжить работу.
Два способа снять блокировку:
В ваших сниппетах недостаточно контекста, чтобы решить, какой из них лучше.
Обратите внимание, что у вас также может быть потенциальная гонка на таймере, он не отображается в вашем фрагменте. Но обратный вызов может выполняться через микросекунду после остановки таймера. Избегайте такой головной боли, используя реальный поток вместо обратного вызова таймера. Он может делать что-то периодически, вызывая WaitOne () в ManualResetEvent, передавая значение тайм-аута. Это событие ManualResetEvent хорошо сигнализирует об остановке потока.