1. Почему вы зашли в тупик
Сначала краткий ответ: вы пропустили сброс настроек.
Я скопировал ваш код (изменил скобки на мой предпочтительный стиль), и я объясню проблему в комментариях:
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 выше) с четырьмя различными видами поведения и сложностями.В целом, мы можем сделать вывод, что разработка с использованием многопоточности может быть очень сложной задачей, когда нам нужно четко понимать свои цели и осознавать, что может пойти не так на каждом шагу.Вы можете сказать, что можно быть немного параноиком, и это относится не только к многопоточности.