Я чувствую, что должен знать ответ на это, но я собираюсь спросить так или иначе на всякий случай, что я делаю потенциально катастрофическую ошибку.
Следующий код выполняется как ожидалось без ошибок/исключений:
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
, даже при том, что документация ясно указывает что попытка сделать что-либо после a Close
приведет к неопределенному поведению.
Мой вопрос, когда я вызываю ManualResetEvent.Set
метод, это, как гарантируют, будет сигнализировать обо всех потоках ожидания перед возвращением управления вызывающей стороне? Другими словами, предположение, что я в состоянии гарантировать, что не будет никаких дальнейших вызовов к WaitOne
, действительно ли безопасно закрыть дескриптор здесь, или действительно ли возможно, что при некоторых обстоятельствах этот код препятствовал бы некоторым официантам то, чтобы быть сообщенным или результат в ObjectDisposedException
?
В документации только говорится это Set
помещает его в "сообщенное состояние" - это, кажется, не предъявляет претензий о том, когда официанты на самом деле получат тот сигнал, таким образом, я хотел бы быть уверенным.
Когда вы сигнализируете с помощью ManualResetEvent.Set
, вам гарантируется, что все потоки, ожидающие этого события (т.е. state on flag.WaitOne
) будет сигнализироваться перед возвратом управления вызывающей стороне.
Конечно, бывают обстоятельства, когда вы можете установить флаг, и ваш поток не видит его, потому что он выполняет некоторую работу, прежде чем проверяет флаг (или, как предлагается без ошибок, если вы создаете несколько потоков):
ThreadPool.QueueUserWorkItem(s =>
{
QueryDB();
flag.WaitOne();
Console.WriteLine("Work Item 1 Executed");
});
Там является разногласием по флагу, и теперь вы можете создать неопределенное поведение при его закрытии. Ваш флаг - это общий ресурс между вашими потоками, вы должны создать защелку обратного отсчета, на которой каждый поток сигнализирует о завершении. Это устранит конфликт по вашему флагу
.
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% безопасна в многопоточной среде.
ОБНОВЛЕНИЕ: Один производитель / несколько потребителей
Предполагается, что ваш производитель знает, сколько потребителей будет создано, После создания всех ваших потребителей вы сбрасываете 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
Мне кажется рискованным шаблоном, даже если из-за с [текущей] реализацией, все в порядке. вы пытаетесь избавиться от ресурса, который все еще может использоваться.
Это похоже на создание и создание объекта, а также на его слепое удаление еще до того, как будут выполнены потребители этого объекта.
Даже в противном случае здесь есть проблема . Программа может завершиться даже до того, как другие потоки когда-либо получат возможность работать. Потоки пула потоков - это фоновые потоки.
Учитывая, что вам в любом случае нужно дождаться других потоков, вы можете после этого выполнить очистку.
Это не нормально. Здесь вам повезло, потому что вы запустили только два потока. Они немедленно начнут работать, когда вы вызовете 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();
}
Ваш исходный код будет терпеть неудачу на старой или текущей машине, когда она очень занята другими задачами.
Я считаю, что здесь имеет место состояние гонки. Написав объекты событий на основе переменных условий, вы получаете код, подобный этому:
mutex.lock();
while (!signalled)
condition_variable.wait(mutex);
mutex.unlock();
Таким образом, хотя событие может быть сигнализировано, код, ожидающий события, все еще может нуждаться в доступе к частям события.
Согласно документации по Close, это освобождает только неуправляемые ресурсы. Так что если событие использует только управляемые ресурсы, вам может повезти. Но в будущем ситуация может измениться, поэтому я бы предпочел проявить осторожность и не закрывать событие, пока не убедитесь, что оно больше не используется.