Действительно ли безопасно сигнализировать и сразу закрыть 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, даже при том, что документация ясно указывает что попытка сделать что-либо после a Close приведет к неопределенному поведению.

Мой вопрос, когда я вызываю ManualResetEvent.Set метод, это, как гарантируют, будет сигнализировать обо всех потоках ожидания перед возвращением управления вызывающей стороне? Другими словами, предположение, что я в состоянии гарантировать, что не будет никаких дальнейших вызовов к WaitOne, действительно ли безопасно закрыть дескриптор здесь, или действительно ли возможно, что при некоторых обстоятельствах этот код препятствовал бы некоторым официантам то, чтобы быть сообщенным или результат в ObjectDisposedException?

В документации только говорится это Set помещает его в "сообщенное состояние" - это, кажется, не предъявляет претензий о том, когда официанты на самом деле получат тот сигнал, таким образом, я хотел бы быть уверенным.

11
задан Aaronaught 25 February 2010 в 20:11
поделиться

4 ответа

Когда вы сигнализируете с помощью 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();
    }
}
  1. Каждый поток сигнализирует о защелке обратного отсчета.
  2. Ваш основной поток ожидает защелки обратного отсчета.
  3. Основной поток очищается после сигналов защелки обратного отсчета.

В конечном счете, время, когда вы спите в конце, НЕ является безопасным способом решения ваших проблем, вместо этого вы должны разработать свою программу так, чтобы она была на 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
6
ответ дан 3 December 2019 в 09:40
поделиться

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

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

Даже в противном случае здесь есть проблема . Программа может завершиться даже до того, как другие потоки когда-либо получат возможность работать. Потоки пула потоков - это фоновые потоки.

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

0
ответ дан 3 December 2019 в 09:40
поделиться

Это не нормально. Здесь вам повезло, потому что вы запустили только два потока. Они немедленно начнут работать, когда вы вызовете 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();
    }

Ваш исходный код будет терпеть неудачу на старой или текущей машине, когда она очень занята другими задачами.

5
ответ дан 3 December 2019 в 09:40
поделиться

Я считаю, что здесь имеет место состояние гонки. Написав объекты событий на основе переменных условий, вы получаете код, подобный этому:

mutex.lock();
while (!signalled)
    condition_variable.wait(mutex);
mutex.unlock();

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

Согласно документации по Close, это освобождает только неуправляемые ресурсы. Так что если событие использует только управляемые ресурсы, вам может повезти. Но в будущем ситуация может измениться, поэтому я бы предпочел проявить осторожность и не закрывать событие, пока не убедитесь, что оно больше не используется.

0
ответ дан 3 December 2019 в 09:40
поделиться
Другие вопросы по тегам:

Похожие вопросы: