Безопасно ли сигнализировать и сразу закрыть 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

Похож на рискованный шаблон для меня, даже если из-за [текущей] реализации это нормально. вы пытаетесь распорядиться ресурсом, который все еще может быть использован.

Это похоже на новизну и создание объекта и удаление его вслепую даже до того, как пользователи этого объекта будут сделаны.

В противном случае здесь существует проблема. Программа может выйти, даже до того, как у других потоков появилась возможность запуска. Нити пула потоков являются фоновыми потоками.

Учитывая, что вам все равно придется ждать других потоков, вы также можете очистить их впоследствии.