Тупик при блокировке ручного сброса события
Я сталкиваюсь с тупиком, вызванным при блокировке экземпляра manualResetEvent. Я не могу понять, как это решить. Я буду признателен за любую помощь.
У меня есть 2 метода в классе, выполняемых разными потоками:
private ManualResetEvent _event = new ManualResetEvent (true);
private void process(){
...
lock(_event){
_event.WaitOne();
...
}
}
internal void Stop(){
_event.Reset();
lock(_event){
...
}
}
Первый поток установил блокировку и заблокирован в _event.WaitOne();
Поток socond выполнил строку _event.Reset(); и блокируется при попытке выполнить блокировку (_event).
Я думал, что когда поток 1 заблокирован на WaitOne, блокировка должна быть снята. Наверное, я не прав. Я не знаю, как я могу это исправить. Кстати, я добавил блокировку, поскольку код в блоке блокировки должен быть синхронизирован в обоих потоках.
Еще раз спасибо и извините за длинный пост.
2 ответа
1. Почему вы попали в тупик
Краткий ответ первым: вы пропустили Сброс для Set.
Я скопировал ваш код (изменил скобки на мой предпочтительный стиль), и я объясню проблему в комментариях:
private ManualResetEvent _event = new ManualResetEvent (true);
private void process()
{
//...
lock(_event)
{
_event.WaitOne(); //Thread A is here waiting _event to be set
//...
}
}
internal void Stop()
{
_event.Reset(); //But thread B just did reset _event
lock(_event) //And know thread B is here waiting... nobody is going to set _event
{
//...
}
}
С этой частью ясно, давайте двигаться дальше к решению.
2. Решение тупика
Так как мы собираемся обменять .Reset()
с .Set()
нам также придется изменить состояние по умолчанию ManualResetEvent
от true
в false
,
Итак, для устранения тупика отредактируйте код следующим образом [проверено]:
private ManualResetEvent _event = new ManualResetEvent (false);
private void process()
{
//...
lock(_event)
{
_event.WaitOne(); //Thread A will be here waiting _event to be set
//...
}
}
internal void Stop()
{
_event.Set(); //And thread B will set it, so thread a can continue
lock(_event) //And when thread a releases the lock on _event thread b can enter
{
//...
}
}
Приведенный выше код не только гарантирует, что только один поток может войти в блокировку одновременно, но и что поток, который входит process
будет ждать, пока есть поток, который вызывает Stop
,
3. Но у вас есть состояние гонки... исправление.
Работа не выполнена, потому что приведенный выше код страдает болезнью состояния гонки. Чтобы понять, почему представьте, что происходит в случае, когда несколько потоков вызывает process
, Только одна нить войдет в замок и будет ждать, пока Stop
вызывается и _event устанавливается, после этого он может продолжаться. Теперь рассмотрим, что произойдет, если поток, вызывающий Stops, будет прерван сразу после вызова _event.Set()
ожидающая нить, которая была в _event.WaitOne()
продолжает и покидает блокировку... теперь вы не можете сказать, что другой поток, который ожидал, чтобы войти в блокировку process
войдет или если поток, который был прерван в Stop
будет продолжать и введите блокировку в этом методе. Это состояние гонки, я не думаю, что вы хотите именно этого.
Тем не менее, я предлагаю вам еще лучшее решение [проверено]:
private ManualResetEvent _event = new ManualResetEvent (false);
private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();
private void process()
{
//...
_readWrite.EnterReadLock();
_event.WaitOne();
try
{
//...
}
finally
{
_readWrite.ExitReadLock();
}
}
internal void Stop()
{
//there are three relevant thread positions at the process method:
//a) before _readWrite.EnterReadLock();
//b) before _event.WaitOne();
//c) after _readWrite.EnterReadLock();
_event.Set(); //Threads at position b start to advance
Thread.Sleep(1); //We want this thread to preempt now!
_event.Reset(); //And here we stop them
//Threads at positions a and b wait where they are
//We wait for any threads at position c
_readWrite.EnterWriteLock();
try
{
//...
}
finally
{
_readWrite.ExitWriteLock();
//Now the threads in position a continues...
// but are halted at position b
//Any thread in position b will wait until Stop is called again
}
}
Прочитайте комментарии в коде, чтобы понять, как это работает. Проще говоря, требуется преимущество блокировки чтения-записи, чтобы позволить нескольким потокам войти в метод process
но только один, чтобы войти Stop
, Хотя была проделана дополнительная работа, чтобы гарантировать, что потоки, которые вызывают метод process
будет ждать, пока поток вызовет метод Stop
,
4. И теперь у вас есть проблема с повторным входом... ее устранение.
Решение выше - лучше... и это не значит идеально. Что с этим не так? Хорошо, если вы вызываете Stop рекурсивно или если вы вызываете его из двух разных потоков одновременно, это не будет работать правильно, потому что второй вызов может создать потоки при продвижении процесса, пока выполняется первый вызов... и я думаю, что вы не не хочу этого Он действительно имел возможность, что с блокировкой чтения-записи было достаточно, чтобы предотвратить проблемы из-за нескольких потоков, вызывающих метод Stop
, но это было не так.
Чтобы решить эту проблему, мы должны убедиться, что Stop выполняется только один раз за раз. Вы можете сделать это с помощью блокировки:
private ManualResetEvent _event = new ManualResetEvent (false);
private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();
//I'm going to use _syncroot, you can use any object...
// as long as you don't lock on it somewhere else
private object _syncroot = new object();
private void process()
{
//...
_readWrite.EnterReadLock();
_event.WaitOne();
try
{
//...
}
finally
{
_readWrite.ExitReadLock();
}
}
internal void Stop()
{
lock(_syncroot)
{
//there are three relevant thread positions at the process method:
//a) before _readWrite.EnterReadLock();
//b) before _event.WaitOne();
//c) after _readWrite.EnterReadLock();
_event.Set(); //Threads at position b start to advance
Thread.Sleep(1); //We want this thread to preempt now!
_event.Reset(); //And here we stop them
//Threads at positions a and b wait where they are
//We wait for any threads at position c
_readWrite.EnterWriteLock();
try
{
//...
}
finally
{
_readWrite.ExitWriteLock();
//Now the threads in position a continues...
// but are halted at position b
//Any thread in position b will wait until Stop is called again
}
}
}
Зачем нам нужна блокировка чтения-записи? - вы можете спросить - если мы используем блокировку, чтобы гарантировать, что только один поток входит в метод Stop
...?
Поскольку блокировка чтения-записи также позволяет потоку в методе Stop
остановить новые потоки, вызывающие метод process
позволяя тем, кто уже был там, выполнить и дождаться их окончания.
Зачем нам ManualResetEvent
? - вы можете спросить - если у нас уже есть блокировка чтения-записи для контроля выполнения потоков в методе process
...?
Потому что блокировка чтения-записи не способна предотвратить выполнение кода в методе process
перед методом Stop
был вызван.
Итак, ты крошечный, нам нужно все это... или мы?
Ну, это зависит от того, как вы себя ведете, поэтому в случае, если я решу проблему, а не то, что у вас было, я предлагаю несколько альтернативных решений ниже.
5. Альтернативное решение с альтернативным поведением
Блокировку очень легко понять, но на мой вкус это слишком много, особенно если нет необходимости следить за тем, чтобы каждый одновременный вызов Stop имел возможность разрешить выполнение потока в методе. process
,
Если это так, то вы можете переписать код следующим образом:
private ManualResetEvent _event = new ManualResetEvent (false);
private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();
private int _stopGuard;
private void process()
{
//...
_readWrite.EnterReadLock();
_event.WaitOne();
try
{
//...
}
finally
{
_readWrite.ExitReadLock();
}
}
internal void Stop()
{
if(Interlocked.CompareExchange(_stopGuard, 1, 0) == 0)
{
//there are three relevant thread positions at the process method:
//a) before _readWrite.EnterReadLock();
//b) before _event.WaitOne();
//c) after _readWrite.EnterReadLock();
_event.Set(); //Threads at position b start to advance
Thread.Sleep(1); //We want this thread to preempt now!
_event.Reset(); //And here we stop them
//Threads at positions a and b wait where they are
//We wait for any threads at position c
_readWrite.EnterWriteLock();
try
{
//...
}
finally
{
_readWrite.ExitWriteLock();
//Now the threads in position a continues...
// but are halted at position b
//Any thread in position b will wait until Stop is called again
}
}
}
Еще не правильное поведение? Хорошо, давайте посмотрим на другое.
6. Альтернативное решение с альтернативным поведением... снова
На этот раз мы увидим, как разрешить нескольким потокам вводить метод process
еще до метода Stop
назывался.
private ReaderWriterLockSlim _readWrite = new ReaderWriterLockSlim();
private int _stopGuard;
private void process()
{
//...
_readWrite.EnterReadLock();
try
{
//...
}
finally
{
_readWrite.ExitReadLock();
}
}
internal void Stop()
{
if(Interlocked.CompareExchange(_stopGuard, 1, 0) == 0)
{
//there are two relevant thread positions at the process method:
//a) before _readWrite.EnterReadLock();
//b) after _readWrite.EnterReadLock();
//We wait for any threads at position b
_readWrite.EnterWriteLock();
try
{
//...
}
finally
{
_readWrite.ExitWriteLock();
//Now the threads in position a continues...
// and they will continue until halted when Stop is called again
}
}
}
Не то, что вы хотите?
Хорошо, я сдаюсь... давайте вернемся к основам.
7. А что ты уже знал
... для полноты, если вам нужно только убедиться, что доступ к обоим методам синхронизирован, и вы можете разрешить запуск методов в процессе в любое время, то вы можете сделать это только с помощью блокировок... и Вы уже знали это.
private object _syncroot = new object();
private void process()
{
//...
lock(_syncroot)
{
//...
}
}
internal void Stop()
{
lock(_syncroot)
{
//...
}
}
7. Заключение
Мы увидели, почему возникла тупиковая ситуация и как ее устранить, но также обнаружили, что отсутствие тупиковой ситуации не является гарантией безопасности потоков. Наконец, мы увидели три решения (пункты 4, 5, 6 и 7 выше) с четырьмя различными видами поведения и сложностями. В целом, мы можем сделать вывод, что разработка с использованием многопоточности может быть очень сложной задачей, когда нам нужно четко понимать свои цели и осознавать, что может пойти не так на каждом шагу. Вы можете сказать, что можно быть немного параноиком, и это относится не только к многопоточности.
Я предполагаю, что вы запутались с Monitor.Wait(объект) и ManualResetEvent.WaitOne ().
Monitor.Wait(объект) снимает блокировку и ждет, пока она не получит блокировку. ManualResetEvent.WaitOne() блокирует текущий поток, пока дескриптор события не получит сигнал.
Я также рекомендую не использовать объект ManualResetEvent в качестве блокировки одновременно. Даже если компилятор не сгенерирует ошибку, это, скорее всего, приведет к путанице, как это может произойти сейчас.