Безопасно ли сигнализировать и сразу закрыть ManualResetEvent?
Я чувствую, что должен знать ответ на этот вопрос, но я все равно спрошу, если я сделаю потенциально катастрофическую ошибку.
Следующий код выполняется, как ожидалось, без ошибок/исключений:
static void Main(string[] args)
{
ManualResetEvent flag = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(s =>
{
flag.WaitOne();
Console.WriteLine("Work Item 1 Executed");
});
ThreadPool.QueueUserWorkItem(s =>
{
flag.WaitOne();
Console.WriteLine("Work Item 2 Executed");
});
Thread.Sleep(1000);
flag.Set();
flag.Close();
Console.WriteLine("Finished");
}
Конечно, как обычно бывает с многопоточным кодом, успешный тест не доказывает, что это фактически потокобезопасный. Тест также завершится успешно, если я положил Close
до Set
, хотя в документации четко указано, что попытка сделать что-либо после Close
приведет к поведению undefined.
Мой вопрос в том, что когда я вызываю метод ManualResetEvent.Set
, гарантирован ли он сигнализировать все ожидания потоков, прежде чем возвращать управление вызывающему абоненту? Другими словами, если предположить, что я могу гарантировать, что дальнейших вызовов на WaitOne
не будет, безопасно ли здесь закрыть дескриптор или возможно, что в некоторых случаях этот код помешает некоторым официантам получить сигнал или результат ObjectDisposedException
?
В документации только говорится, что Set
помещает его в состояние "сигнализированное" - он, похоже, не претендует на то, что официанты действительно получат этот сигнал, поэтому я хотел бы быть уверен.
Ответы
Ответ 1
Когда вы сигнализируете с помощью ManualResetEvent.Set
, вам гарантируется, что все потоки, ожидающие этого события (т.е. в состоянии блокировки на flag.WaitOne
), будут сигнализированы, прежде чем возвращать управление вызывающему абоненту.
Конечно, есть обстоятельства, когда вы можете установить флаг, и ваш поток не видит его, потому что он выполняет некоторую работу до того, как он проверит флаг (или, как предположили значки, если вы создаете несколько потоков):
ThreadPool.QueueUserWorkItem(s =>
{
QueryDB();
flag.WaitOne();
Console.WriteLine("Work Item 1 Executed");
});
На флаге есть разногласия, и теперь вы можете создать поведение undefined при его закрытии. Ваш флаг является общим ресурсом между вашими потоками, вы должны создать защелку обратного отсчета, по которой каждый поток сигнализирует о завершении. Это устранит разногласия на вашем flag
.
public class CountdownLatch
{
private int m_remain;
private EventWaitHandle m_event;
public CountdownLatch(int count)
{
Reset(count);
}
public void Reset(int count)
{
if (count < 0)
throw new ArgumentOutOfRangeException();
m_remain = count;
m_event = new ManualResetEvent(false);
if (m_remain == 0)
{
m_event.Set();
}
}
public void Signal()
{
// The last thread to signal also sets the event.
if (Interlocked.Decrement(ref m_remain) == 0)
m_event.Set();
}
public void Wait()
{
m_event.WaitOne();
}
}
- Каждый поток сигнализирует о защелке обратного отсчета.
- Ваш основной поток ждет обратного отсчета.
- Основной поток очищается после сигналов фиксации обратного отсчета.
В конечном счете, когда вы спите в конце, НЕ безопасный способ позаботиться о своих проблемах, вместо этого вы должны разработать свою программу, чтобы она была на 100% безопасной в многопоточной среде.
ОБНОВЛЕНИЕ: Одиночный производитель/несколько потребителей
Предполагается, что ваш продюсер знает, сколько потребителей будет создано,. После создания всех ваших потребителей вы reset CountdownLatch
с заданным количеством потребителей:
// In the Producer
ManualResetEvent flag = new ManualResetEvent(false);
CountdownLatch countdown = new CountdownLatch(0);
int numConsumers = 0;
while(hasMoreWork)
{
Consumer consumer = new Consumer(coutndown, flag);
// Create a new thread with each consumer
numConsumers++;
}
countdown.Reset(numConsumers);
flag.Set();
countdown.Wait();// your producer waits for all of the consumers to finish
flag.Close();// cleanup
Ответ 2
Это не нормально. Вам повезло, потому что вы только начали два потока. Они сразу начнут работать, когда вы вызовете Set на двухъядерной машине. Попробуйте это вместо этого и посмотрите, как он бомбит:
static void Main(string[] args) {
ManualResetEvent flag = new ManualResetEvent(false);
for (int ix = 0; ix < 10; ++ix) {
ThreadPool.QueueUserWorkItem(s => {
flag.WaitOne();
Console.WriteLine("Work Item Executed");
});
}
Thread.Sleep(1000);
flag.Set();
flag.Close();
Console.WriteLine("Finished");
Console.ReadLine();
}
Исходный код будет сбой аналогичным образом на старой машине или на вашем текущем компьютере, когда он очень занят другими задачами.
Ответ 3
Я считаю, что есть состояние гонки. Создав объекты событий на основе переменных условия, вы получите код следующим образом:
mutex.lock();
while (!signalled)
condition_variable.wait(mutex);
mutex.unlock();
Таким образом, пока событие может быть сигнализировано, ожидающий код события может по-прежнему нуждаться в доступе к частям события.
Согласно документации на Close, это освобождает неуправляемые ресурсы. Поэтому, если событие использует только управляемые ресурсы, вам может повезти. Но это может измениться в будущем, поэтому я буду ошибаться на стороне предосторожности и не закрывать мероприятие, пока вы не узнаете, что его больше не используют.
Ответ 4
Похож на рискованный шаблон для меня, даже если из-за [текущей] реализации это нормально. вы пытаетесь распорядиться ресурсом, который все еще может быть использован.
Это похоже на новизну и создание объекта и удаление его вслепую даже до того, как пользователи этого объекта будут сделаны.
В противном случае здесь существует проблема. Программа может выйти, даже до того, как у других потоков появилась возможность запуска. Нити пула потоков являются фоновыми потоками.
Учитывая, что вам все равно придется ждать других потоков, вы также можете очистить их впоследствии.